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 CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natureco-cli",
3
- "version": "5.7.1",
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"
@@ -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
+ };
@@ -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
- module.exports = {
6
- name: 'read_file',
7
- description: 'PRIMARY TOOL: Read a specific file content. Use this when user wants to read/view/look at a single file. For listing directories use filesystem tool.',
8
- inputSchema: {
9
- type: 'object',
10
- properties: {
11
- path: {
12
- type: 'string',
13
- description: 'File path to read'
14
- }
15
- },
16
- required: ['path']
17
- },
18
-
19
- async execute(params) {
20
- try {
21
- // v5.2.0: Robust path expansion (~, ~/, relative)
22
- const { expandPath } = require('../utils/paths');
23
- const filePath = expandPath(params.path);
24
-
25
- if (!fs.existsSync(filePath)) {
26
- return {
27
- success: false,
28
- error: 'File does not exist'
29
- };
30
- }
31
-
32
- const stats = fs.statSync(filePath);
33
-
34
- if (!stats.isFile()) {
35
- return {
36
- success: false,
37
- error: 'Path is not a file'
38
- };
39
- }
40
-
41
- // Check file size - if > 1MB, read only first 50KB
42
- if (stats.size > 1024 * 1024) {
43
- const fd = fs.openSync(filePath, 'r');
44
- const buf = Buffer.alloc(50000);
45
- fs.readSync(fd, buf, 0, 50000, 0);
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: false
70
+ truncated: true,
67
71
  };
68
- } catch (error) {
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: false,
71
- error: error.message
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
  };
@@ -1,88 +1,255 @@
1
1
  /**
2
- * todo_write - Yapilacaklar listesi (v4.9.0)
2
+ * todo_write Task / todo manager.
3
3
  *
4
- * Hermes todo_write'una benzer.
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("fs");
8
- const path = require("path");
9
- const os = require("os");
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 TODO_FILE = path.join(os.homedir(), ".natureco", "todos.json");
23
+ const VALID_STATUSES = new Set(['pending', 'in_progress', 'completed', 'deleted']);
24
+ const VALID_PRIORITIES = new Set(['low', 'medium', 'high']);
12
25
 
13
- function loadTodos() {
14
- try {
15
- if (!fs.existsSync(TODO_FILE)) return [];
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 saveTodos(todos) {
31
+ function _saveTodos(todos) {
21
32
  fs.mkdirSync(path.dirname(TODO_FILE), { recursive: true });
22
- fs.writeFileSync(TODO_FILE, JSON.stringify(todos, null, 2), "utf8");
33
+ writeJsonAtomicSync(TODO_FILE, todos);
23
34
  }
24
35
 
25
- function genId() {
36
+ function _genId() {
26
37
  return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
27
38
  }
28
39
 
29
- async function todoAction({ action = "list", content, id, priority = "medium", status }) {
30
- let todos = loadTodos();
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
- if (action === "list") {
33
- const pending = todos.filter(t => t.status === "pending");
34
- const done = todos.filter(t => t.status === "done");
35
- return { success: true, total: todos.length, pending: pending.length, done: done.length, todos: pending };
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
- if (action === "add") {
39
- if (!content) return { success: false, error: "content gerekli" };
40
- const todo = { id: genId(), content, priority, status: "pending", createdAt: new Date().toISOString() };
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
- saveTodos(todos);
43
- return { success: true, todo, message: `Todo eklendi: ${content}` };
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
- if (action === "done") {
47
- if (!id) return { success: false, error: "id gerekli" };
48
- const todo = todos.find(t => t.id === id);
49
- if (!todo) return { success: false, error: `Todo bulunamadi: ${id}` };
50
- todo.status = "done";
51
- todo.completedAt = new Date().toISOString();
52
- saveTodos(todos);
53
- return { success: true, todo, message: `Tamamlandi: ${todo.content}` };
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
- if (action === "remove") {
57
- if (!id) return { success: false, error: "id gerekli" };
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
- saveTodos(todos);
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 === "clear") {
65
- saveTodos([]);
66
- return { success: true, cleared: todos.length, message: "Tum todolar temizlendi" };
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: "todo_write",
74
- description: "Yapilacaklar listesi. action: list, add, done, remove, clear.",
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: "object",
231
+ type: 'object',
77
232
  properties: {
78
- action: { type: "string", description: "list/add/done/remove/clear (default: list)", enum: ["list", "add", "done", "remove", "clear"] },
79
- content: { type: "string", description: "Todo icerigi (add icin)" },
80
- id: { type: "string", description: "Todo ID (done/remove icin)" },
81
- priority: { type: "string", description: "Oncelik: low/medium/high (add icin)", enum: ["low", "medium", "high"] },
82
- status: { type: "string", description: "Durum filtresi: pending/done" },
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
- return await todoAction(params);
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
+ };