natureco-cli 5.7.0 → 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.
@@ -8,53 +8,93 @@
8
8
  const fs = require("fs");
9
9
  const path = require("path");
10
10
  const os = require("os");
11
+ const { writeJsonAtomicSync, readJsonSafeSync } = require("../utils/atomic-file");
11
12
 
12
13
  const MEMORY_DIR = path.join(os.homedir(), ".natureco", "memory");
13
14
 
15
+ // Soft cap on per-user fact count. Decay already prunes old/low-importance
16
+ // facts; this just bounds the worst-case file size and prompt-injection
17
+ // surface. Configurable via NATURECO_MAX_FACTS (default 50, was a hard 15
18
+ // that was silently truncating new writes when full).
19
+ const MAX_FACTS_PER_USER = (() => {
20
+ const raw = parseInt(process.env.NATURECO_MAX_FACTS || "", 10);
21
+ return Number.isFinite(raw) && raw > 0 ? raw : 50;
22
+ })();
23
+
14
24
  function getMemoryFile(username) {
15
25
  const name = (username || "default").toLowerCase();
16
26
  return path.join(MEMORY_DIR, `${name}.json`);
17
27
  }
18
28
 
29
+ function _emptyMemory(username) {
30
+ return { name: username || "User", nickname: null, botName: null, facts: [], preferences: [] };
31
+ }
32
+
19
33
  function loadMemory(username) {
20
- const file = getMemoryFile(username);
21
- try {
22
- if (!fs.existsSync(file)) {
23
- return { name: username || "User", nickname: null, botName: null, facts: [], preferences: [] };
24
- }
25
- return JSON.parse(fs.readFileSync(file, "utf8"));
26
- } catch {
27
- return { name: username || "User", nickname: null, botName: null, facts: [], preferences: [] };
28
- }
34
+ return readJsonSafeSync(getMemoryFile(username), _emptyMemory(username));
29
35
  }
30
36
 
31
37
  function saveMemory(username, memory) {
32
38
  if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });
33
39
  memory.lastUpdated = new Date().toISOString();
34
- fs.writeFileSync(getMemoryFile(username), JSON.stringify(memory, null, 2), "utf8");
40
+ writeJsonAtomicSync(getMemoryFile(username), memory);
35
41
  return memory;
36
42
  }
37
43
 
38
44
  /**
39
- * Score azalt (eski fact'ler zamanla unutuluyor)
45
+ * Score azalt (eski fact'ler zamanla unutulur).
46
+ * Sıralama veya soft-cap UYGULAMAZ — onu enforceFactLimit yapar
47
+ * (push'tan sonra çağrılır, böylece yeni fact silinmez).
40
48
  */
41
49
  function decayFacts(memory) {
42
50
  if (!memory.facts) return memory;
43
51
  const now = Date.now();
44
52
  memory.facts = memory.facts.map(f => {
45
53
  if (!f.score) f.score = 5;
46
- // 1 haftadan eski -1, 1 aydan eski -3
47
54
  const ageMs = now - new Date(f.updatedAt || f.createdAt || now).getTime();
48
55
  const ageDays = ageMs / (1000 * 60 * 60 * 24);
49
56
  if (ageDays > 30) f.score = Math.max(0, f.score - 3);
50
57
  else if (ageDays > 7) f.score = Math.max(0, f.score - 1);
51
58
  return f;
52
59
  });
53
- // score 0 olanlari sil
54
60
  memory.facts = memory.facts.filter(f => (f.score || 0) > 0);
55
- // max 15 fact tut
56
- memory.facts.sort((a, b) => (b.score || 0) - (a.score || 0));
57
- memory.facts = memory.facts.slice(0, 15);
61
+ return memory;
62
+ }
63
+
64
+ /**
65
+ * Soft cap: yeni fact eklenmesinden SONRA uygulanır, böylece az önce
66
+ * yazılan fact eviction kurbanı olmaz. En düşük score + en eski updatedAt
67
+ * önce gider. Cap aşılırsa stderr'e tek satır warn yazar (eski "silent
68
+ * truncate to 15" davranışını gözlemlenebilir hale getirir).
69
+ *
70
+ * @param {{facts: Array<object>}} memory
71
+ * @param {{recentValue?: string}} [opts] Korunması zorunlu fact (yeni eklenen)
72
+ * @returns the same memory
73
+ */
74
+ function enforceFactLimit(memory, opts = {}) {
75
+ if (!memory.facts || memory.facts.length <= MAX_FACTS_PER_USER) return memory;
76
+ const before = memory.facts.length;
77
+ const recent = opts.recentValue ? opts.recentValue.toLowerCase() : null;
78
+ // Sort by score desc, then updatedAt desc (newest+highest first).
79
+ // The just-pushed fact is pinned at the top regardless of its score.
80
+ memory.facts.sort((a, b) => {
81
+ if (recent) {
82
+ if ((a.value || "").toLowerCase() === recent) return -1;
83
+ if ((b.value || "").toLowerCase() === recent) return 1;
84
+ }
85
+ const sa = a.score || 0, sb = b.score || 0;
86
+ if (sa !== sb) return sb - sa;
87
+ return String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""));
88
+ });
89
+ memory.facts = memory.facts.slice(0, MAX_FACTS_PER_USER);
90
+ const dropped = before - memory.facts.length;
91
+ if (dropped > 0 && !process.env.NATURECO_QUIET_MEMORY) {
92
+ // eslint-disable-next-line no-console
93
+ console.warn(
94
+ `[memory] cap ${MAX_FACTS_PER_USER} aşıldı, en düşük skorlu ${dropped} fact düşürüldü ` +
95
+ `(NATURECO_MAX_FACTS ile değiştir, NATURECO_QUIET_MEMORY=1 ile sustur)`
96
+ );
97
+ }
58
98
  return memory;
59
99
  }
60
100
 
@@ -122,6 +162,8 @@ function addMemory({ username, fact, score = 5, category = "general", botName, n
122
162
  createdAt: new Date().toISOString(),
123
163
  });
124
164
  }
165
+ // Limit'i ZIM push'tan sonra uygula → yeni eklenen fact düşürülmez.
166
+ memory = enforceFactLimit(memory, { recentValue: fact });
125
167
  }
126
168
 
127
169
  if (!memory.preferences) memory.preferences = [];
@@ -173,6 +215,18 @@ function showMemory({ username }) {
173
215
  }
174
216
 
175
217
  module.exports = {
218
+ // Exposed for tests / advanced consumers — not part of the tool schema.
219
+ _internals: {
220
+ MAX_FACTS_PER_USER,
221
+ enforceFactLimit,
222
+ decayFacts,
223
+ loadMemory,
224
+ saveMemory,
225
+ addMemory,
226
+ clearMemory,
227
+ showMemory,
228
+ getMemoryFile,
229
+ },
176
230
  name: "memory_write",
177
231
  description: "Memory'ye yeni fact/bilgi kaydet veya bot ismini/nickname'i degistir. Kalici, REPL'in extractMemoryFromMessage ozelligi.",
178
232
  inputSchema: {
@@ -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
+ };