natureco-cli 5.7.1 → 5.7.2
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/bin/natureco.js +0 -0
- package/package.json +1 -1
- package/src/commands/repl.js +6 -2
- package/src/tools/edit_file.js +143 -0
- package/src/tools/read_file.js +122 -63
- package/src/tools/todo_write.js +219 -52
- package/src/utils/paste-safe-input.js +173 -0
package/bin/natureco.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "natureco-cli",
|
|
3
|
-
"version": "5.7.
|
|
3
|
+
"version": "5.7.2",
|
|
4
4
|
"description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"natureco": "bin/natureco.js"
|
package/src/commands/repl.js
CHANGED
|
@@ -24,6 +24,7 @@ const chalk = require('chalk');
|
|
|
24
24
|
const tui = require('../utils/tui');
|
|
25
25
|
const { loadToolDefinitions, toOpenAIFormat, executeTool } = require('../utils/tools');
|
|
26
26
|
const { accumulateToolCallDeltas, finalizeToolCalls } = require('../utils/streaming-tools');
|
|
27
|
+
const { createPasteSafeInput, enableBracketedPaste, disableBracketedPaste, restoreNewlines } = require('../utils/paste-safe-input');
|
|
27
28
|
|
|
28
29
|
// v5.4.6: Model adi sizintisini engelle — global'e ata, callback'lerden erisebilir olsun
|
|
29
30
|
const MODEL_NAMES_TO_HIDE = ['MiniMax-M2.5', 'MiniMaxM2.5', 'minimaxm25', 'Claude-3', 'GPT-4', 'ChatGPT'];
|
|
@@ -615,8 +616,10 @@ async function startRepl(args) {
|
|
|
615
616
|
let totalInputTokens = 0;
|
|
616
617
|
let totalOutputTokens = 0;
|
|
617
618
|
|
|
619
|
+
enableBracketedPaste(process.stdout);
|
|
620
|
+
|
|
618
621
|
const rl = readline.createInterface({
|
|
619
|
-
input: process.stdin,
|
|
622
|
+
input: createPasteSafeInput(process.stdin),
|
|
620
623
|
output: process.stdout,
|
|
621
624
|
prompt: tui.styled('\n You ', { color: tui.PALETTE.primary, bold: true }),
|
|
622
625
|
terminal: true,
|
|
@@ -643,6 +646,7 @@ async function startRepl(args) {
|
|
|
643
646
|
}
|
|
644
647
|
// Global buffer temizle
|
|
645
648
|
if (global._fixBuffer) global._fixBuffer = '';
|
|
649
|
+
disableBracketedPaste(process.stdout);
|
|
646
650
|
console.log(chalk.gray('\n 👋 Görüşürüz!\n'));
|
|
647
651
|
process.exit(exitCode);
|
|
648
652
|
};
|
|
@@ -721,7 +725,7 @@ async function startRepl(args) {
|
|
|
721
725
|
process.on('SIGTERM', () => cleanup(0));
|
|
722
726
|
|
|
723
727
|
rl.on('line', async (input) => {
|
|
724
|
-
const line = input.trim();
|
|
728
|
+
const line = restoreNewlines(input).trim();
|
|
725
729
|
if (!line) { rl.prompt(); return; }
|
|
726
730
|
|
|
727
731
|
// Slash komutlar
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* edit_file — Precise string replacement in an existing file.
|
|
3
|
+
*
|
|
4
|
+
* Modeled after Claude Code's `Edit` tool semantics, with three rules
|
|
5
|
+
* that catch ~all "the agent broke my file" failure modes:
|
|
6
|
+
*
|
|
7
|
+
* 1. The file must exist (no accidental file creation via Edit — use
|
|
8
|
+
* write_file for that).
|
|
9
|
+
* 2. `old_string` must appear in the file. If it doesn't, return an
|
|
10
|
+
* error pointing at the file path; do NOT silently no-op.
|
|
11
|
+
* 3. `old_string` must be UNIQUE in the file unless `replace_all: true`
|
|
12
|
+
* is passed. Otherwise the agent would mutate an unintended site
|
|
13
|
+
* without realizing it.
|
|
14
|
+
*
|
|
15
|
+
* On success: writes via atomic rename (no half-edited files on crash),
|
|
16
|
+
* returns the patched line count + a small diff summary.
|
|
17
|
+
*
|
|
18
|
+
* Why this exists in natureco: until this commit, natureco's only file
|
|
19
|
+
* mutation tool was `write_file`, which overwrites the entire file. Any
|
|
20
|
+
* "rename X to Y in src/foo.js" request meant the agent had to read the
|
|
21
|
+
* file, do the substitution in its head, and re-emit the whole file —
|
|
22
|
+
* a process that loses indentation, drops trailing newlines, and
|
|
23
|
+
* occasionally hallucinates unrelated edits. `edit_file` makes the
|
|
24
|
+
* mutation a 2-3 line delta the agent can't get wrong.
|
|
25
|
+
*/
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { writeFileAtomicSync } = require('../utils/atomic-file');
|
|
29
|
+
|
|
30
|
+
function _expand(p) {
|
|
31
|
+
if (!p) return p;
|
|
32
|
+
if (p.startsWith('~')) {
|
|
33
|
+
return path.join(require('os').homedir(), p.slice(1));
|
|
34
|
+
}
|
|
35
|
+
return path.resolve(p);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function editFile({ path: filePath, old_string, new_string, replace_all = false }) {
|
|
39
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
40
|
+
return { success: false, error: 'path is required (absolute path or ~-prefixed)' };
|
|
41
|
+
}
|
|
42
|
+
if (typeof old_string !== 'string' || old_string.length === 0) {
|
|
43
|
+
return { success: false, error: 'old_string is required and cannot be empty' };
|
|
44
|
+
}
|
|
45
|
+
if (typeof new_string !== 'string') {
|
|
46
|
+
return { success: false, error: 'new_string is required (use empty string to delete)' };
|
|
47
|
+
}
|
|
48
|
+
if (old_string === new_string) {
|
|
49
|
+
return { success: false, error: 'old_string and new_string are identical — nothing to do' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const target = _expand(filePath);
|
|
53
|
+
if (!fs.existsSync(target)) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
error: `file not found: ${target}. Edit cannot create files; use write_file for that.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
let original;
|
|
60
|
+
try {
|
|
61
|
+
original = fs.readFileSync(target, 'utf8');
|
|
62
|
+
} catch (e) {
|
|
63
|
+
return { success: false, error: `read failed: ${e.message}` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Find occurrences. Use indexOf loop for an accurate count without
|
|
67
|
+
// pulling in regex escaping bugs (old_string is raw text, not a pattern).
|
|
68
|
+
let count = 0;
|
|
69
|
+
let i = 0;
|
|
70
|
+
while ((i = original.indexOf(old_string, i)) !== -1) {
|
|
71
|
+
count++;
|
|
72
|
+
i += old_string.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (count === 0) {
|
|
76
|
+
// Help the agent debug: include a small excerpt so it sees how close
|
|
77
|
+
// its old_string is to reality (whitespace, casing).
|
|
78
|
+
const excerpt = original.length > 400 ? original.slice(0, 400) + '\n... (truncated)' : original;
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: 'old_string not found in file',
|
|
82
|
+
path: target,
|
|
83
|
+
hint: 'Verify exact whitespace, casing, and that you Read the file recently.',
|
|
84
|
+
file_excerpt: excerpt,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (count > 1 && !replace_all) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
error: `old_string is not unique (${count} occurrences). Pass replace_all: true to change every instance, or extend old_string with more surrounding context to make it unique.`,
|
|
92
|
+
path: target,
|
|
93
|
+
occurrences: count,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const updated = replace_all
|
|
98
|
+
? original.split(old_string).join(new_string)
|
|
99
|
+
: original.replace(old_string, new_string);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
writeFileAtomicSync(target, updated);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return { success: false, error: `write failed: ${e.message}` };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const linesBefore = original.split('\n').length;
|
|
108
|
+
const linesAfter = updated.split('\n').length;
|
|
109
|
+
const lineDelta = linesAfter - linesBefore;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
success: true,
|
|
113
|
+
path: target,
|
|
114
|
+
replacements: count,
|
|
115
|
+
replace_all: !!replace_all,
|
|
116
|
+
lines_before: linesBefore,
|
|
117
|
+
lines_after: linesAfter,
|
|
118
|
+
line_delta: lineDelta,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
name: 'edit_file',
|
|
124
|
+
description:
|
|
125
|
+
'Performs exact string replacement in an existing file. The old_string ' +
|
|
126
|
+
'must match the file content EXACTLY (including whitespace and indentation) ' +
|
|
127
|
+
'and must be UNIQUE unless replace_all=true. Use this for any targeted ' +
|
|
128
|
+
'rename/refactor/patch instead of rewriting the file with write_file. ' +
|
|
129
|
+
'Fails with a helpful hint when old_string is missing or ambiguous.',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
path: { type: 'string', description: 'Absolute path of file to edit (~/-prefix allowed)' },
|
|
134
|
+
old_string: { type: 'string', description: 'Exact substring to replace (case-sensitive, whitespace-sensitive)' },
|
|
135
|
+
new_string: { type: 'string', description: 'Replacement text (must differ from old_string; pass "" to delete)' },
|
|
136
|
+
replace_all: { type: 'boolean', description: 'Replace every occurrence instead of failing on multiple matches', default: false },
|
|
137
|
+
},
|
|
138
|
+
required: ['path', 'old_string', 'new_string'],
|
|
139
|
+
},
|
|
140
|
+
execute: editFile,
|
|
141
|
+
// Exposed for unit tests.
|
|
142
|
+
_internals: { editFile },
|
|
143
|
+
};
|
package/src/tools/read_file.js
CHANGED
|
@@ -1,75 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* read_file — Read a file's contents.
|
|
3
|
+
*
|
|
4
|
+
* Backwards-compatible upgrade (v5.7.1): the old call shape
|
|
5
|
+
* `{ path }` still works and still returns `{success, path, content, size,
|
|
6
|
+
* truncated}` exactly as before. The new options add Claude Code-style
|
|
7
|
+
* pagination + line-numbered output:
|
|
8
|
+
*
|
|
9
|
+
* - `offset` (line number, 1-based): skip the first N-1 lines.
|
|
10
|
+
* - `limit` (line count): read at most this many lines.
|
|
11
|
+
* - `numbered` (bool): if true, prefix each line with its 1-based
|
|
12
|
+
* line number + a tab (matches `cat -n`); useful when the agent
|
|
13
|
+
* then needs to call edit_file and wants to cite exact lines.
|
|
14
|
+
*
|
|
15
|
+
* Other safety touches retained from the original:
|
|
16
|
+
* - File-existence + not-a-file checks.
|
|
17
|
+
* - 1 MB cap before falling into "show me 50 KB" mode (raised to
|
|
18
|
+
* 2 MB now that line-based pagination exists — the agent can ask
|
|
19
|
+
* for `{offset, limit}` instead).
|
|
20
|
+
* - Returns `{success: false, error}` rather than throwing.
|
|
21
|
+
*/
|
|
1
22
|
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
23
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
fs.closeSync(fd);
|
|
47
|
-
|
|
48
|
-
const content = '[Büyük dosya - ilk ~50KB gösteriliyor]\n' + buf.toString('utf8');
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
success: true,
|
|
52
|
-
path: filePath,
|
|
53
|
-
content,
|
|
54
|
-
size: stats.size,
|
|
55
|
-
truncated: true
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
60
|
-
|
|
24
|
+
const DEFAULT_LINE_LIMIT = 2000; // matches Claude Code's Read default
|
|
25
|
+
const HARD_BYTE_CAP = 2 * 1024 * 1024; // 2 MB — agent should paginate above this
|
|
26
|
+
const SOFT_BYTE_PREVIEW = 50_000; // bytes shown for the "file too large" preview
|
|
27
|
+
|
|
28
|
+
function _formatNumbered(text, startLine) {
|
|
29
|
+
const lines = text.split('\n');
|
|
30
|
+
let out = '';
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
out += String(startLine + i) + '\t' + lines[i];
|
|
33
|
+
if (i < lines.length - 1) out += '\n';
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readFile(params) {
|
|
39
|
+
try {
|
|
40
|
+
const { expandPath } = require('../utils/paths');
|
|
41
|
+
const filePath = expandPath(params.path);
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(filePath)) {
|
|
44
|
+
return { success: false, error: 'File does not exist' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const stats = fs.statSync(filePath);
|
|
48
|
+
if (!stats.isFile()) {
|
|
49
|
+
return { success: false, error: 'Path is not a file' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const offset = Math.max(1, Number(params.offset) || 1);
|
|
53
|
+
const limit = params.limit !== undefined ? Math.max(1, Number(params.limit)) : null;
|
|
54
|
+
const numbered = !!params.numbered;
|
|
55
|
+
const wantsPagination = params.offset !== undefined || params.limit !== undefined;
|
|
56
|
+
|
|
57
|
+
// Large-file guard. With pagination the agent can read what it needs;
|
|
58
|
+
// without it we still show a useful preview (matches old behavior).
|
|
59
|
+
if (stats.size > HARD_BYTE_CAP && !wantsPagination) {
|
|
60
|
+
const fd = fs.openSync(filePath, 'r');
|
|
61
|
+
const buf = Buffer.alloc(SOFT_BYTE_PREVIEW);
|
|
62
|
+
fs.readSync(fd, buf, 0, SOFT_BYTE_PREVIEW, 0);
|
|
63
|
+
fs.closeSync(fd);
|
|
64
|
+
const preview = '[Büyük dosya — ilk ~50KB gösteriliyor; offset/limit ile sayfalayın]\n' + buf.toString('utf8');
|
|
61
65
|
return {
|
|
62
66
|
success: true,
|
|
63
67
|
path: filePath,
|
|
64
|
-
content,
|
|
68
|
+
content: preview,
|
|
65
69
|
size: stats.size,
|
|
66
|
-
truncated:
|
|
70
|
+
truncated: true,
|
|
67
71
|
};
|
|
68
|
-
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
75
|
+
|
|
76
|
+
if (!wantsPagination && !numbered) {
|
|
77
|
+
// Original return shape — keep byte-for-byte compatible.
|
|
69
78
|
return {
|
|
70
|
-
success:
|
|
71
|
-
|
|
79
|
+
success: true,
|
|
80
|
+
path: filePath,
|
|
81
|
+
content: raw,
|
|
82
|
+
size: stats.size,
|
|
83
|
+
truncated: false,
|
|
72
84
|
};
|
|
73
85
|
}
|
|
86
|
+
|
|
87
|
+
// Pagination + optional line numbers.
|
|
88
|
+
const allLines = raw.split('\n');
|
|
89
|
+
const totalLines = allLines.length;
|
|
90
|
+
const start = Math.min(offset, totalLines + 1) - 1; // to 0-based
|
|
91
|
+
const effectiveLimit = limit !== null ? limit : DEFAULT_LINE_LIMIT;
|
|
92
|
+
const end = Math.min(start + effectiveLimit, totalLines);
|
|
93
|
+
const slice = allLines.slice(start, end);
|
|
94
|
+
const sliceText = slice.join('\n');
|
|
95
|
+
const content = numbered ? _formatNumbered(sliceText, start + 1) : sliceText;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
path: filePath,
|
|
100
|
+
content,
|
|
101
|
+
size: stats.size,
|
|
102
|
+
truncated: end < totalLines,
|
|
103
|
+
total_lines: totalLines,
|
|
104
|
+
lines_returned: slice.length,
|
|
105
|
+
offset: start + 1,
|
|
106
|
+
limit: effectiveLimit,
|
|
107
|
+
numbered,
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return { success: false, error: error.message };
|
|
74
111
|
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
name: 'read_file',
|
|
116
|
+
description:
|
|
117
|
+
'PRIMARY TOOL: Read a file. Supports Claude Code-style pagination ' +
|
|
118
|
+
'(offset + limit, 1-based line numbers) and optional `numbered` output ' +
|
|
119
|
+
'(prefixes each line with its number + tab, matching `cat -n`). ' +
|
|
120
|
+
'For listing directories use the filesystem tool. For ranges past 2000 ' +
|
|
121
|
+
'lines, call again with a new offset.',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
path: { type: 'string', description: 'File path to read (absolute or ~-prefix)' },
|
|
126
|
+
offset: { type: 'integer', description: '1-based line number to start at (default 1)', minimum: 1 },
|
|
127
|
+
limit: { type: 'integer', description: `Maximum lines to return (default ${DEFAULT_LINE_LIMIT})`, minimum: 1 },
|
|
128
|
+
numbered: { type: 'boolean', description: 'Prefix each line with its 1-based line number + tab (cat -n format)', default: false },
|
|
129
|
+
},
|
|
130
|
+
required: ['path'],
|
|
131
|
+
},
|
|
132
|
+
execute: readFile,
|
|
133
|
+
_internals: { readFile, _formatNumbered },
|
|
75
134
|
};
|
package/src/tools/todo_write.js
CHANGED
|
@@ -1,88 +1,255 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* todo_write
|
|
2
|
+
* todo_write — Task / todo manager.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Upgrade (v5.7.1) modeled after Claude Code's TaskCreate/TaskUpdate/
|
|
5
|
+
* TaskList semantics so the agent can track multi-step work like
|
|
6
|
+
* `pending → in_progress → completed`, name an `activeForm` for the
|
|
7
|
+
* spinner, and express dependencies via `blockedBy` / `blocks`.
|
|
8
|
+
*
|
|
9
|
+
* Backwards compatibility: every old action (list/add/done/remove/
|
|
10
|
+
* clear) and the old `{content, priority, status: 'done'}` shape keeps
|
|
11
|
+
* working byte-for-byte — see the test file for the locked-in shape.
|
|
12
|
+
* The on-disk JSON gains new fields but old `{id, content, status,
|
|
13
|
+
* priority, createdAt, completedAt}` entries are still read correctly.
|
|
5
14
|
*/
|
|
6
15
|
|
|
7
|
-
const fs = require(
|
|
8
|
-
const path = require(
|
|
9
|
-
const os = require(
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const { writeJsonAtomicSync, readJsonSafeSync } = require('../utils/atomic-file');
|
|
20
|
+
|
|
21
|
+
const TODO_FILE = path.join(os.homedir(), '.natureco', 'todos.json');
|
|
10
22
|
|
|
11
|
-
const
|
|
23
|
+
const VALID_STATUSES = new Set(['pending', 'in_progress', 'completed', 'deleted']);
|
|
24
|
+
const VALID_PRIORITIES = new Set(['low', 'medium', 'high']);
|
|
12
25
|
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return JSON.parse(fs.readFileSync(TODO_FILE, "utf8"));
|
|
17
|
-
} catch { return []; }
|
|
26
|
+
function _loadTodos() {
|
|
27
|
+
const data = readJsonSafeSync(TODO_FILE, []);
|
|
28
|
+
return Array.isArray(data) ? data : [];
|
|
18
29
|
}
|
|
19
30
|
|
|
20
|
-
function
|
|
31
|
+
function _saveTodos(todos) {
|
|
21
32
|
fs.mkdirSync(path.dirname(TODO_FILE), { recursive: true });
|
|
22
|
-
|
|
33
|
+
writeJsonAtomicSync(TODO_FILE, todos);
|
|
23
34
|
}
|
|
24
35
|
|
|
25
|
-
function
|
|
36
|
+
function _genId() {
|
|
26
37
|
return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
27
38
|
}
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
function _normalizeStatus(s) {
|
|
41
|
+
if (!s) return null;
|
|
42
|
+
// accept legacy 'done' as alias for 'completed'
|
|
43
|
+
if (s === 'done') return 'completed';
|
|
44
|
+
return VALID_STATUSES.has(s) ? s : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _normalizeTodo(t) {
|
|
48
|
+
// Migrate old shape on the fly so listing always returns a uniform
|
|
49
|
+
// structure even for entries written by v5.6 and earlier.
|
|
50
|
+
return {
|
|
51
|
+
id: t.id,
|
|
52
|
+
subject: t.subject || t.content || '(no subject)',
|
|
53
|
+
description: t.description || '',
|
|
54
|
+
activeForm: t.activeForm || t.subject || t.content || '',
|
|
55
|
+
status: _normalizeStatus(t.status) || 'pending',
|
|
56
|
+
priority: VALID_PRIORITIES.has(t.priority) ? t.priority : 'medium',
|
|
57
|
+
owner: t.owner || null,
|
|
58
|
+
blockedBy: Array.isArray(t.blockedBy) ? t.blockedBy : [],
|
|
59
|
+
blocks: Array.isArray(t.blocks) ? t.blocks : [],
|
|
60
|
+
metadata: (t.metadata && typeof t.metadata === 'object') ? t.metadata : {},
|
|
61
|
+
createdAt: t.createdAt || new Date().toISOString(),
|
|
62
|
+
updatedAt: t.updatedAt || t.createdAt || new Date().toISOString(),
|
|
63
|
+
completedAt: t.completedAt || null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
31
66
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
67
|
+
function _isBlocked(todo, allTodos) {
|
|
68
|
+
if (!todo.blockedBy || todo.blockedBy.length === 0) return false;
|
|
69
|
+
const openIds = new Set(
|
|
70
|
+
allTodos.filter(t => t.status !== 'completed' && t.status !== 'deleted').map(t => t.id),
|
|
71
|
+
);
|
|
72
|
+
return todo.blockedBy.some(id => openIds.has(id));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function todoAction(params) {
|
|
76
|
+
const action = params.action || 'list';
|
|
77
|
+
let todos = _loadTodos().map(_normalizeTodo);
|
|
78
|
+
|
|
79
|
+
// ─────────────────── LIST / GET ───────────────────
|
|
80
|
+
if (action === 'list' || action === 'get') {
|
|
81
|
+
if (action === 'get') {
|
|
82
|
+
if (!params.id) return { success: false, error: 'id gerekli' };
|
|
83
|
+
const t = todos.find(x => x.id === params.id);
|
|
84
|
+
if (!t) return { success: false, error: `Todo bulunamadı: ${params.id}` };
|
|
85
|
+
return { success: true, todo: t };
|
|
86
|
+
}
|
|
87
|
+
// Filter by status if specified
|
|
88
|
+
const filtered = params.status
|
|
89
|
+
? todos.filter(t => t.status === _normalizeStatus(params.status))
|
|
90
|
+
: todos.filter(t => t.status !== 'deleted');
|
|
91
|
+
// Annotate with blockedBy state
|
|
92
|
+
const annotated = filtered.map(t => ({
|
|
93
|
+
...t,
|
|
94
|
+
currently_blocked: _isBlocked(t, todos),
|
|
95
|
+
}));
|
|
96
|
+
return {
|
|
97
|
+
success: true,
|
|
98
|
+
total: todos.filter(t => t.status !== 'deleted').length,
|
|
99
|
+
pending: todos.filter(t => t.status === 'pending').length,
|
|
100
|
+
in_progress: todos.filter(t => t.status === 'in_progress').length,
|
|
101
|
+
completed: todos.filter(t => t.status === 'completed').length,
|
|
102
|
+
todos: annotated,
|
|
103
|
+
};
|
|
36
104
|
}
|
|
37
105
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
106
|
+
// ─────────────────── ADD / CREATE ───────────────────
|
|
107
|
+
if (action === 'add' || action === 'create') {
|
|
108
|
+
const subject = params.subject || params.content;
|
|
109
|
+
if (!subject) return { success: false, error: 'subject (veya content) gerekli' };
|
|
110
|
+
const todo = _normalizeTodo({
|
|
111
|
+
id: _genId(),
|
|
112
|
+
subject,
|
|
113
|
+
description: params.description || '',
|
|
114
|
+
activeForm: params.activeForm || subject,
|
|
115
|
+
status: 'pending',
|
|
116
|
+
priority: params.priority || 'medium',
|
|
117
|
+
owner: params.owner || null,
|
|
118
|
+
blockedBy: Array.isArray(params.blockedBy) ? params.blockedBy : [],
|
|
119
|
+
blocks: Array.isArray(params.blocks) ? params.blocks : [],
|
|
120
|
+
metadata: params.metadata || {},
|
|
121
|
+
createdAt: new Date().toISOString(),
|
|
122
|
+
updatedAt: new Date().toISOString(),
|
|
123
|
+
});
|
|
41
124
|
todos.push(todo);
|
|
42
|
-
|
|
43
|
-
|
|
125
|
+
// Bidirectional link: if A blocks B, B is blockedBy A.
|
|
126
|
+
for (const blockedId of todo.blocks) {
|
|
127
|
+
const other = todos.find(t => t.id === blockedId);
|
|
128
|
+
if (other && !other.blockedBy.includes(todo.id)) {
|
|
129
|
+
other.blockedBy.push(todo.id);
|
|
130
|
+
other.updatedAt = new Date().toISOString();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
_saveTodos(todos);
|
|
134
|
+
return { success: true, todo, message: `Görev eklendi: ${subject}` };
|
|
44
135
|
}
|
|
45
136
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
137
|
+
// ─────────────────── UPDATE / START / DONE / REOPEN ───────────────────
|
|
138
|
+
if (action === 'update' || action === 'start' || action === 'done' || action === 'completed' || action === 'reopen') {
|
|
139
|
+
if (!params.id) return { success: false, error: 'id gerekli' };
|
|
140
|
+
const idx = todos.findIndex(t => t.id === params.id);
|
|
141
|
+
if (idx < 0) return { success: false, error: `Todo bulunamadı: ${params.id}` };
|
|
142
|
+
|
|
143
|
+
const todo = todos[idx];
|
|
144
|
+
|
|
145
|
+
if (action === 'start') {
|
|
146
|
+
// Refuse to start if blocked
|
|
147
|
+
if (_isBlocked(todo, todos)) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
error: `Görev bloklu — şu açık görevler bitmeden başlayamaz: ${todo.blockedBy.join(', ')}`,
|
|
151
|
+
blockedBy: todo.blockedBy,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
todo.status = 'in_progress';
|
|
155
|
+
} else if (action === 'done' || action === 'completed') {
|
|
156
|
+
todo.status = 'completed';
|
|
157
|
+
todo.completedAt = new Date().toISOString();
|
|
158
|
+
} else if (action === 'reopen') {
|
|
159
|
+
todo.status = 'pending';
|
|
160
|
+
todo.completedAt = null;
|
|
161
|
+
} else if (action === 'update') {
|
|
162
|
+
if (params.subject !== undefined) todo.subject = params.subject;
|
|
163
|
+
if (params.description !== undefined) todo.description = params.description;
|
|
164
|
+
if (params.activeForm !== undefined) todo.activeForm = params.activeForm;
|
|
165
|
+
if (params.priority !== undefined && VALID_PRIORITIES.has(params.priority)) {
|
|
166
|
+
todo.priority = params.priority;
|
|
167
|
+
}
|
|
168
|
+
if (params.status !== undefined) {
|
|
169
|
+
const ns = _normalizeStatus(params.status);
|
|
170
|
+
if (ns) todo.status = ns;
|
|
171
|
+
if (ns === 'completed') todo.completedAt = new Date().toISOString();
|
|
172
|
+
}
|
|
173
|
+
if (params.owner !== undefined) todo.owner = params.owner;
|
|
174
|
+
if (Array.isArray(params.addBlockedBy)) {
|
|
175
|
+
for (const blockId of params.addBlockedBy) {
|
|
176
|
+
if (!todo.blockedBy.includes(blockId)) todo.blockedBy.push(blockId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (Array.isArray(params.addBlocks)) {
|
|
180
|
+
for (const blockedId of params.addBlocks) {
|
|
181
|
+
if (!todo.blocks.includes(blockedId)) todo.blocks.push(blockedId);
|
|
182
|
+
const other = todos.find(t => t.id === blockedId);
|
|
183
|
+
if (other && !other.blockedBy.includes(todo.id)) other.blockedBy.push(todo.id);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (params.metadata && typeof params.metadata === 'object') {
|
|
187
|
+
for (const [k, v] of Object.entries(params.metadata)) {
|
|
188
|
+
if (v === null) delete todo.metadata[k];
|
|
189
|
+
else todo.metadata[k] = v;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
todo.updatedAt = new Date().toISOString();
|
|
194
|
+
_saveTodos(todos);
|
|
195
|
+
return { success: true, todo, message: `Güncellendi: ${todo.subject}` };
|
|
54
196
|
}
|
|
55
197
|
|
|
56
|
-
|
|
57
|
-
|
|
198
|
+
// ─────────────────── REMOVE / CLEAR ───────────────────
|
|
199
|
+
if (action === 'remove' || action === 'delete') {
|
|
200
|
+
if (!params.id) return { success: false, error: 'id gerekli' };
|
|
58
201
|
const before = todos.length;
|
|
59
|
-
todos = todos.filter(t => t.id !== id);
|
|
60
|
-
|
|
202
|
+
todos = todos.filter(t => t.id !== params.id);
|
|
203
|
+
// Also clean up any blockedBy references that now point at nothing
|
|
204
|
+
for (const t of todos) {
|
|
205
|
+
t.blockedBy = t.blockedBy.filter(bid => todos.some(x => x.id === bid));
|
|
206
|
+
t.blocks = t.blocks.filter(bid => todos.some(x => x.id === bid));
|
|
207
|
+
}
|
|
208
|
+
_saveTodos(todos);
|
|
61
209
|
return { success: true, removed: before - todos.length };
|
|
62
210
|
}
|
|
63
211
|
|
|
64
|
-
if (action ===
|
|
65
|
-
|
|
66
|
-
|
|
212
|
+
if (action === 'clear') {
|
|
213
|
+
const n = todos.length;
|
|
214
|
+
_saveTodos([]);
|
|
215
|
+
return { success: true, cleared: n, message: 'Tüm görevler temizlendi' };
|
|
67
216
|
}
|
|
68
217
|
|
|
69
218
|
return { success: false, error: `Bilinmeyen action: ${action}` };
|
|
70
219
|
}
|
|
71
220
|
|
|
72
221
|
module.exports = {
|
|
73
|
-
name:
|
|
74
|
-
description:
|
|
222
|
+
name: 'todo_write',
|
|
223
|
+
description:
|
|
224
|
+
'Görev / todo yöneticisi. action: list, get, add (alias: create), ' +
|
|
225
|
+
'update, start (pending→in_progress, blokluysa reddeder), ' +
|
|
226
|
+
'done (alias: completed), reopen, remove (alias: delete), clear. ' +
|
|
227
|
+
'Yeni alanlar: subject, description, activeForm (spinner için), ' +
|
|
228
|
+
'priority, owner, blockedBy, blocks, metadata. Legacy {content, ' +
|
|
229
|
+
'status: "done"} çağrıları hala geçerli.',
|
|
75
230
|
inputSchema: {
|
|
76
|
-
type:
|
|
231
|
+
type: 'object',
|
|
77
232
|
properties: {
|
|
78
|
-
action: {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
233
|
+
action: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description: 'list / get / add / create / update / start / done / completed / reopen / remove / delete / clear',
|
|
236
|
+
enum: ['list', 'get', 'add', 'create', 'update', 'start', 'done', 'completed', 'reopen', 'remove', 'delete', 'clear'],
|
|
237
|
+
},
|
|
238
|
+
id: { type: 'string', description: 'Görev ID (get/update/start/done/reopen/remove için)' },
|
|
239
|
+
content: { type: 'string', description: 'LEGACY: subject ile aynı, geriye uyumluluk için tutuluyor' },
|
|
240
|
+
subject: { type: 'string', description: 'Kısa başlık (zorunlu, add için)' },
|
|
241
|
+
description: { type: 'string', description: 'Uzun açıklama (opsiyonel)' },
|
|
242
|
+
activeForm: { type: 'string', description: 'Devam ederken gösterilen form (örn: "Testleri çalıştırıyor")' },
|
|
243
|
+
status: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'done', 'deleted'] },
|
|
244
|
+
priority: { type: 'string', enum: ['low', 'medium', 'high'] },
|
|
245
|
+
owner: { type: 'string', description: 'Atanan agent/kullanıcı adı' },
|
|
246
|
+
blockedBy: { type: 'array', items: { type: 'string' }, description: 'Bu göreve önce tamamlanması gerekenler' },
|
|
247
|
+
blocks: { type: 'array', items: { type: 'string' }, description: 'Bu görev tamamlanmadan başlayamayacaklar (ters yönlü blockedBy)' },
|
|
248
|
+
addBlockedBy: { type: 'array', items: { type: 'string' }, description: 'update için: mevcut blockedBy listesine ekle' },
|
|
249
|
+
addBlocks: { type: 'array', items: { type: 'string' }, description: 'update için: mevcut blocks listesine ekle' },
|
|
250
|
+
metadata: { type: 'object', description: 'Arbitrary key/value notlar (update için: null ile sil)' },
|
|
83
251
|
},
|
|
84
252
|
},
|
|
85
|
-
async execute(params) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
};
|
|
253
|
+
async execute(params) { return todoAction(params); },
|
|
254
|
+
_internals: { todoAction, _normalizeTodo, _isBlocked, _genId, TODO_FILE },
|
|
255
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paste-safe-input — Terminal'e uzun/çok satırlı metin yapıştırılınca
|
|
3
|
+
* her satırın ayrı bir "Enter" gibi gönderilip anında submit edilmesini önler.
|
|
4
|
+
*
|
|
5
|
+
* Sorun: Node'un düz `readline` arayüzü, terminalden gelen her "\n" karakterini
|
|
6
|
+
* bir 'line' event'i olarak görür. Terminal "bracketed paste mode" açık değilse
|
|
7
|
+
* (veya açık olsa da bu karakterler ayıklanmazsa), kullanıcı 10 satırlık bir metni
|
|
8
|
+
* yapıştırdığında readline bunu 10 ayrı mesaj gibi okur ve her birini hemen
|
|
9
|
+
* gönderir — kullanıcı paste'i bitirmeden cevaplar gelmeye başlar.
|
|
10
|
+
*
|
|
11
|
+
* Çözüm: Terminalin "bracketed paste" (ESC[200~ ... ESC[201~) işaretleyicilerini
|
|
12
|
+
* dinleyip, bu işaretler arasındaki tüm veriyi tek parça olarak topluyoruz.
|
|
13
|
+
* İçindeki gerçek satır sonlarını geçici bir placeholder ile değiştirip
|
|
14
|
+
* readline'a TEK satır gibi besliyoruz. Kullanıcı gerçekten Enter'a basana kadar
|
|
15
|
+
* hiçbir 'line' event'i tetiklenmez. Enter'a basıldığında, alınan satırdaki
|
|
16
|
+
* placeholder'lar gerçek "\n" karakterine geri çevrilip mesaj olarak kullanılır.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const { PassThrough } = require('stream');
|
|
22
|
+
|
|
23
|
+
const PASTE_START = '\x1b[200~';
|
|
24
|
+
const PASTE_END = '\x1b[201~';
|
|
25
|
+
|
|
26
|
+
// Yapıştırılan içindeki satır sonlarını gizlemek için kullanılan placeholder.
|
|
27
|
+
// Gerçek kullanıcı girdisinde pratikte hiç geçmeyecek bir dizi.
|
|
28
|
+
const NEWLINE_PLACEHOLDER = '\u2424\u2424LINEBREAK\u2424\u2424';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Bracketed paste'i destekleyen bir PassThrough stream döner.
|
|
32
|
+
* `source` (genellikle process.stdin) üzerinden gelen veriyi işler.
|
|
33
|
+
*
|
|
34
|
+
* readline.createInterface({ input: createPasteSafeInput(process.stdin), ... })
|
|
35
|
+
* şeklinde process.stdin yerine kullanılmalı.
|
|
36
|
+
*/
|
|
37
|
+
// str'nin sonunda PASTE_START veya PASTE_END marker'ının bir ön-eki (prefix)
|
|
38
|
+
// olarak duran kısmın uzunluğunu döner (0 = eşleşme yok). Sadece gerçekten
|
|
39
|
+
// marker olabilecek bir kuyruk varsa bekletiyoruz; aksi halde tek tuş
|
|
40
|
+
// yazımında karakterler bir sonraki tuşa kadar ekranda gecikir.
|
|
41
|
+
function trailingMarkerPrefixLen(str, marker) {
|
|
42
|
+
const maxLen = Math.min(str.length, marker.length - 1);
|
|
43
|
+
for (let len = maxLen; len > 0; len--) {
|
|
44
|
+
if (str.slice(str.length - len) === marker.slice(0, len)) return len;
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// TERMINAL-AGNOSTIK PASTE TESPİTİ
|
|
50
|
+
// ---------------------------------
|
|
51
|
+
// Birçok terminal (örn. macOS'un yerleşik Terminal.app'i) bracketed-paste
|
|
52
|
+
// (ESC[200~/[201~) modunu desteklemez/etkinleştirmez. Bu yüzden bracket
|
|
53
|
+
// marker'larına güvenmek yetersiz kalıyor. Daha güvenilir, terminalden
|
|
54
|
+
// bağımsız bir sinyal var: bir gerçek Enter tuşuna basış, işletim sisteminden
|
|
55
|
+
// tek başına ve tek bir karakter ('\r' veya '\n') olarak gelir — başka hiçbir
|
|
56
|
+
// karakterle birlikte değil. Ama bir yapıştırma (paste), terminal emülatörü
|
|
57
|
+
// tarafından TEK SEFERDE (çok karakterli, içinde satır sonları barındıran bir
|
|
58
|
+
// "data" event'i olarak) pty'ye yazılır. Yani: içinde satır sonu BARINDIRAN
|
|
59
|
+
// ama SADECE tek bir satır sonu karakterinden İBARET OLMAYAN bir chunk,
|
|
60
|
+
// hemen hemen kesin olarak bir paste'tir — bracket marker olsun olmasın.
|
|
61
|
+
function isLoneEnterKeystroke(str) {
|
|
62
|
+
return str === '\r' || str === '\n' || str === '\r\n';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function escapeEmbeddedNewlines(str) {
|
|
66
|
+
if (!str || isLoneEnterKeystroke(str)) return str;
|
|
67
|
+
const newlineCount = (str.match(/\r\n|\r|\n/g) || []).length;
|
|
68
|
+
// Terminal-agnostik paste tespiti: sadece içinde birden çok satır
|
|
69
|
+
// barındıran chunk'lar paste kabul edilir. Tek satır + \n normal
|
|
70
|
+
// yazım olabilir (test/paste-safe-input.test.js "normal yazılan
|
|
71
|
+
// satırlar ayrı ayrı submit edilir" senaryosu).
|
|
72
|
+
if (newlineCount === 0) return str;
|
|
73
|
+
if (newlineCount === 1 && str.length < 100) return str;
|
|
74
|
+
return str.replace(/\r\n|\r|\n/g, NEWLINE_PLACEHOLDER);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createPasteSafeInput(source = process.stdin) {
|
|
78
|
+
const proxy = new PassThrough();
|
|
79
|
+
|
|
80
|
+
let inPaste = false;
|
|
81
|
+
let carry = ''; // marker'ın chunk sınırında bölünmesine karşı tampon
|
|
82
|
+
|
|
83
|
+
const onData = (chunk) => {
|
|
84
|
+
let str = carry + chunk.toString('utf8');
|
|
85
|
+
carry = '';
|
|
86
|
+
let out = '';
|
|
87
|
+
|
|
88
|
+
while (str.length) {
|
|
89
|
+
if (inPaste) {
|
|
90
|
+
const endIdx = str.indexOf(PASTE_END);
|
|
91
|
+
if (endIdx === -1) {
|
|
92
|
+
const keepLen = trailingMarkerPrefixLen(str, PASTE_END);
|
|
93
|
+
const flushLen = str.length - keepLen;
|
|
94
|
+
out += str.slice(0, flushLen).replace(/\r\n|\r|\n/g, NEWLINE_PLACEHOLDER);
|
|
95
|
+
carry = str.slice(flushLen);
|
|
96
|
+
str = '';
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
const pasted = str.slice(0, endIdx);
|
|
100
|
+
out += pasted.replace(/\r\n|\r|\n/g, NEWLINE_PLACEHOLDER);
|
|
101
|
+
inPaste = false;
|
|
102
|
+
str = str.slice(endIdx + PASTE_END.length);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const startIdx = str.indexOf(PASTE_START);
|
|
107
|
+
if (startIdx === -1) {
|
|
108
|
+
const keepLen = trailingMarkerPrefixLen(str, PASTE_START);
|
|
109
|
+
const flushLen = str.length - keepLen;
|
|
110
|
+
// Bracket marker'ı olmayan terminallerde de paste'i yakala: bu segment
|
|
111
|
+
// tek başına bir Enter tuşu değilse ve içinde satır sonu varsa, o
|
|
112
|
+
// satır sonlarını gizle (terminal-agnostik heuristik).
|
|
113
|
+
out += escapeEmbeddedNewlines(str.slice(0, flushLen));
|
|
114
|
+
carry = str.slice(flushLen);
|
|
115
|
+
str = '';
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
out += escapeEmbeddedNewlines(str.slice(0, startIdx));
|
|
119
|
+
str = str.slice(startIdx + PASTE_START.length);
|
|
120
|
+
inPaste = true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (out) proxy.write(out);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
source.on('data', onData);
|
|
127
|
+
source.on('end', () => {
|
|
128
|
+
if (carry) {
|
|
129
|
+
proxy.write(inPaste ? carry.replace(/\r\n|\r|\n/g, NEWLINE_PLACEHOLDER) : carry);
|
|
130
|
+
carry = '';
|
|
131
|
+
}
|
|
132
|
+
proxy.end();
|
|
133
|
+
});
|
|
134
|
+
source.on('error', (e) => proxy.emit('error', e));
|
|
135
|
+
|
|
136
|
+
// process.stdin'in raw-mode/pause-resume API'lerini proxy üzerinden de eriştir,
|
|
137
|
+
// readline bunları çağırabiliyor.
|
|
138
|
+
proxy.isTTY = source.isTTY;
|
|
139
|
+
proxy.setRawMode = source.setRawMode ? source.setRawMode.bind(source) : undefined;
|
|
140
|
+
proxy.ref = source.ref ? source.ref.bind(source) : undefined;
|
|
141
|
+
proxy.unref = source.unref ? source.unref.bind(source) : undefined;
|
|
142
|
+
|
|
143
|
+
return proxy;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Terminalde bracketed paste mode'u açar. Çoğu modern terminal (iTerm2, Terminal.app,
|
|
148
|
+
* Windows Terminal, VS Code, gnome-terminal vb.) bunu destekler. Desteklemeyen
|
|
149
|
+
* terminallerde bu escape kodu sessizce yok sayılır, davranış eskisi gibi kalır.
|
|
150
|
+
*/
|
|
151
|
+
function enableBracketedPaste(out = process.stdout) {
|
|
152
|
+
if (out.isTTY) out.write('\x1b[?2004h');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function disableBracketedPaste(out = process.stdout) {
|
|
156
|
+
if (out.isTTY) out.write('\x1b[?2004l');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* readline'dan gelen satırı, paste placeholder'larını gerçek "\n" karakterine
|
|
161
|
+
* geri çevirerek normalize eder. Gönderilecek mesaj olarak bunu kullan.
|
|
162
|
+
*/
|
|
163
|
+
function restoreNewlines(line) {
|
|
164
|
+
return line.split(NEWLINE_PLACEHOLDER).join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
createPasteSafeInput,
|
|
169
|
+
enableBracketedPaste,
|
|
170
|
+
disableBracketedPaste,
|
|
171
|
+
restoreNewlines,
|
|
172
|
+
NEWLINE_PLACEHOLDER,
|
|
173
|
+
};
|