c0de-agent 1.4.0 → 1.6.0

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.
Files changed (44) hide show
  1. package/dist/core/agent.js +4 -0
  2. package/dist/core/config.js +1 -1
  3. package/dist/core/index.d.ts +3 -3
  4. package/dist/core/index.js +1 -1
  5. package/dist/core/loop.js +9 -0
  6. package/dist/core/prompt-registry.d.ts +2 -2
  7. package/dist/core/prompt-registry.js +42 -3
  8. package/dist/core/slash.js +102 -6
  9. package/dist/core/types.d.ts +10 -1
  10. package/dist/core/workflow.d.ts +1 -1
  11. package/dist/core/workflow.js +56 -5
  12. package/dist/core/workflows/discovery.d.ts +23 -2
  13. package/dist/core/workflows/discovery.js +38 -2
  14. package/dist/core/workflows/index.d.ts +3 -2
  15. package/dist/core/workflows/index.js +2 -2
  16. package/dist/core/workflows/registry.d.ts +8 -1
  17. package/dist/core/workflows/registry.js +21 -1
  18. package/dist/core/workflows/runtime.d.ts +3 -0
  19. package/dist/core/workflows/runtime.js +2 -2
  20. package/dist/project/resolve.d.ts +75 -0
  21. package/dist/project/resolve.js +253 -1
  22. package/dist/server/app.js +3 -0
  23. package/dist/server/context.js +2 -1
  24. package/dist/server/dev.js +3 -1
  25. package/dist/server/routes/chat.js +12 -0
  26. package/dist/server/routes/commands.js +1 -0
  27. package/dist/server/routes/files.js +252 -4
  28. package/dist/server/routes/terminal.js +2 -1
  29. package/dist/server/routes/todo.d.ts +4 -0
  30. package/dist/server/routes/todo.js +107 -0
  31. package/dist/server/routes/workflows.js +83 -10
  32. package/dist/server/server.d.ts +8 -1
  33. package/dist/server/server.js +113 -23
  34. package/dist/server/terminal/pty-manager.d.ts +14 -0
  35. package/dist/server/terminal/pty-manager.js +105 -7
  36. package/dist/shared/types/agent.d.ts +9 -0
  37. package/dist/shared/types/config.d.ts +6 -0
  38. package/dist/shared/types/tool.d.ts +13 -0
  39. package/dist/tools/builtin/todo.d.ts +67 -0
  40. package/dist/tools/builtin/todo.js +517 -0
  41. package/dist/tools/index.d.ts +2 -0
  42. package/dist/tools/index.js +3 -0
  43. package/dist/tools/types.d.ts +32 -1
  44. package/package.json +2 -1
@@ -0,0 +1,517 @@
1
+ // todo tool: phased task tracking with 7 operations, fuzzy matching, and
2
+ // Markdown round-trip. Logic ported from oh-my-pi, adapted to c0de-agent's
3
+ // stateless ToolDef + ToolContext.todoState hook pattern.
4
+ // =============================================================================
5
+ // State helpers
6
+ // =============================================================================
7
+ function findTaskByContent(phases, content) {
8
+ for (const phase of phases) {
9
+ const task = phase.tasks.find((t) => t.content === content);
10
+ if (task)
11
+ return { task, phase };
12
+ }
13
+ return undefined;
14
+ }
15
+ function findPhaseByName(phases, name) {
16
+ return phases.find((phase) => phase.name === name);
17
+ }
18
+ function cloneTask(task) {
19
+ return { content: task.content, status: task.status };
20
+ }
21
+ /** Deep-clone phases (mutation-safe). */
22
+ export function clonePhases(phases) {
23
+ return phases.map((phase) => ({ name: phase.name, tasks: phase.tasks.map(cloneTask) }));
24
+ }
25
+ function todoTransitionKey(phase, content) {
26
+ return `${phase}\u0000${content}`;
27
+ }
28
+ function getCompletionTransitions(previous, updated) {
29
+ const previousStatuses = new Map();
30
+ for (const phase of previous) {
31
+ for (const task of phase.tasks) {
32
+ previousStatuses.set(todoTransitionKey(phase.name, task.content), task.status);
33
+ }
34
+ }
35
+ const transitions = [];
36
+ for (const phase of updated) {
37
+ for (const task of phase.tasks) {
38
+ if (task.status !== 'completed')
39
+ continue;
40
+ const prev = previousStatuses.get(todoTransitionKey(phase.name, task.content));
41
+ if (prev && prev !== 'completed') {
42
+ transitions.push({ phase: phase.name, content: task.content });
43
+ }
44
+ }
45
+ }
46
+ return transitions;
47
+ }
48
+ /** Ensure at most one in_progress task: demote extras, or auto-promote the
49
+ * first pending task if none is in_progress. Mutates in place. */
50
+ function normalizeInProgressTask(phases) {
51
+ const orderedTasks = phases.flatMap((phase) => phase.tasks);
52
+ if (orderedTasks.length === 0)
53
+ return;
54
+ const inProgressTasks = orderedTasks.filter((task) => task.status === 'in_progress');
55
+ if (inProgressTasks.length > 1) {
56
+ for (const task of inProgressTasks.slice(1)) {
57
+ task.status = 'pending';
58
+ }
59
+ }
60
+ if (inProgressTasks.length > 0)
61
+ return;
62
+ const firstPending = orderedTasks.find((task) => task.status === 'pending');
63
+ if (firstPending)
64
+ firstPending.status = 'in_progress';
65
+ }
66
+ /** Return the active todo task, preferring in_progress over the first pending. */
67
+ export function nextActionableTask(phases) {
68
+ let firstPending;
69
+ for (const phase of phases) {
70
+ for (const task of phase.tasks) {
71
+ if (task.status === 'in_progress')
72
+ return task;
73
+ if (!firstPending && task.status === 'pending')
74
+ firstPending = task;
75
+ }
76
+ }
77
+ return firstPending;
78
+ }
79
+ // =============================================================================
80
+ // Fuzzy match
81
+ // =============================================================================
82
+ /** Minimum overlap (after normalization) required for a substring match.
83
+ * Six chars admits single-word identifiers like "review" / "Sonnet" without
84
+ * admitting tiny common substrings like "test" / "fix". */
85
+ const TODO_DESCRIPTION_MIN_OVERLAP = 6;
86
+ function normalizeForTodoMatch(value) {
87
+ return value
88
+ .toLowerCase()
89
+ .replace(/[^\p{L}\p{N}]+/gu, ' ')
90
+ .trim();
91
+ }
92
+ /** Report whether `content` likely names the same work as any entry in
93
+ * `descriptions`. Normalize-then-equal first, with a substring fallback
94
+ * in either direction (≥6 char overlap on the contained side). */
95
+ export function todoMatchesAnyDescription(content, descriptions) {
96
+ const target = normalizeForTodoMatch(content);
97
+ if (!target)
98
+ return false;
99
+ for (const desc of descriptions) {
100
+ const candidate = normalizeForTodoMatch(desc);
101
+ if (!candidate)
102
+ continue;
103
+ if (target === candidate)
104
+ return true;
105
+ if (target.length >= TODO_DESCRIPTION_MIN_OVERLAP && candidate.includes(target))
106
+ return true;
107
+ if (candidate.length >= TODO_DESCRIPTION_MIN_OVERLAP && target.includes(candidate))
108
+ return true;
109
+ }
110
+ return false;
111
+ }
112
+ // =============================================================================
113
+ // Resolution helpers
114
+ // =============================================================================
115
+ function resolveTaskOrError(phases, content, errors) {
116
+ if (!content) {
117
+ errors.push('Missing task content');
118
+ return undefined;
119
+ }
120
+ const hit = findTaskByContent(phases, content);
121
+ if (!hit) {
122
+ if (/^task-\d+$/.test(content)) {
123
+ errors.push(`Task "${content}" not found. Tasks are referenced by content, not by IDs — pass the task's full text from the previous result.`);
124
+ }
125
+ else {
126
+ const totalTasks = phases.reduce((sum, phase) => sum + phase.tasks.length, 0);
127
+ const hint = totalTasks === 0 ? ' (todo list is empty — was it replaced or not yet created?)' : '';
128
+ errors.push(`Task "${content}" not found${hint}`);
129
+ }
130
+ }
131
+ return hit;
132
+ }
133
+ function resolvePhaseOrError(phases, name, errors) {
134
+ if (!name) {
135
+ errors.push('Missing phase name');
136
+ return undefined;
137
+ }
138
+ const phase = findPhaseByName(phases, name);
139
+ if (!phase)
140
+ errors.push(`Phase "${name}" not found`);
141
+ return phase;
142
+ }
143
+ function getTaskTargets(phases, entry, errors) {
144
+ if ('task' in entry && entry.task) {
145
+ const hit = resolveTaskOrError(phases, entry.task, errors);
146
+ return hit ? [hit.task] : [];
147
+ }
148
+ if ('phase' in entry && entry.phase) {
149
+ const phase = resolvePhaseOrError(phases, entry.phase, errors);
150
+ return phase ? [...phase.tasks] : [];
151
+ }
152
+ return phases.flatMap((phase) => phase.tasks);
153
+ }
154
+ // =============================================================================
155
+ // Operations
156
+ // =============================================================================
157
+ /** Phase name for `init` given a flat `items` list with no explicit `phase`. */
158
+ const DEFAULT_INIT_PHASE = 'Tasks';
159
+ function initPhases(entry, errors) {
160
+ // Models routinely flatten single-phase init into {op:"init", items:[...]}
161
+ // instead of the canonical list: [{phase, items}]. Accept that shape.
162
+ const list = entry.list ??
163
+ (entry.items && entry.items.length > 0
164
+ ? [{ phase: entry.phase ?? DEFAULT_INIT_PHASE, items: entry.items }]
165
+ : undefined);
166
+ if (!list) {
167
+ errors.push('Missing list for init operation');
168
+ return [];
169
+ }
170
+ const seenPhases = new Set();
171
+ const seenTasks = new Set();
172
+ for (const listEntry of list) {
173
+ if (seenPhases.has(listEntry.phase)) {
174
+ errors.push(`Duplicate phase "${listEntry.phase}" in init list`);
175
+ }
176
+ seenPhases.add(listEntry.phase);
177
+ for (const content of listEntry.items) {
178
+ if (seenTasks.has(content)) {
179
+ errors.push(`Duplicate task "${content}" in init list`);
180
+ }
181
+ seenTasks.add(content);
182
+ }
183
+ }
184
+ return list.map((listEntry) => ({
185
+ name: listEntry.phase,
186
+ tasks: listEntry.items.map((content) => ({ content, status: 'pending' })),
187
+ }));
188
+ }
189
+ function appendItems(phases, entry, errors) {
190
+ if (!entry.items || entry.items.length === 0) {
191
+ errors.push('Missing items for append operation');
192
+ return phases;
193
+ }
194
+ // Validate the whole batch before mutating.
195
+ const seen = new Set();
196
+ let hasDuplicate = false;
197
+ for (const content of entry.items) {
198
+ if (seen.has(content) || findTaskByContent(phases, content)) {
199
+ errors.push(`Task "${content}" already exists`);
200
+ hasDuplicate = true;
201
+ }
202
+ seen.add(content);
203
+ }
204
+ if (hasDuplicate)
205
+ return phases;
206
+ let phase = findPhaseByName(phases, entry.phase);
207
+ if (!phase) {
208
+ phase = { name: entry.phase, tasks: [] };
209
+ phases.push(phase);
210
+ }
211
+ for (const content of entry.items) {
212
+ phase.tasks.push({ content, status: 'pending' });
213
+ }
214
+ return phases;
215
+ }
216
+ function removeTasks(phases, entry, errors) {
217
+ if (entry.task) {
218
+ const hit = resolveTaskOrError(phases, entry.task, errors);
219
+ if (!hit)
220
+ return phases;
221
+ hit.phase.tasks = hit.phase.tasks.filter((candidate) => candidate !== hit.task);
222
+ return phases;
223
+ }
224
+ if (entry.phase) {
225
+ const phase = resolvePhaseOrError(phases, entry.phase, errors);
226
+ if (!phase)
227
+ return phases;
228
+ phase.tasks = [];
229
+ return phases;
230
+ }
231
+ // No task or phase specified: clear all.
232
+ for (const phase of phases) {
233
+ phase.tasks = [];
234
+ }
235
+ return phases;
236
+ }
237
+ function applyEntry(phases, entry, errors) {
238
+ switch (entry.op) {
239
+ case 'init':
240
+ return initPhases(entry, errors);
241
+ case 'start': {
242
+ const hit = resolveTaskOrError(phases, entry.task, errors);
243
+ if (!hit)
244
+ return phases;
245
+ for (const phase of phases) {
246
+ for (const candidate of phase.tasks) {
247
+ if (candidate.status === 'in_progress' && candidate !== hit.task) {
248
+ candidate.status = 'pending';
249
+ }
250
+ }
251
+ }
252
+ hit.task.status = 'in_progress';
253
+ return phases;
254
+ }
255
+ case 'done': {
256
+ for (const task of getTaskTargets(phases, entry, errors)) {
257
+ task.status = 'completed';
258
+ }
259
+ return phases;
260
+ }
261
+ case 'drop': {
262
+ for (const task of getTaskTargets(phases, entry, errors)) {
263
+ task.status = 'abandoned';
264
+ }
265
+ return phases;
266
+ }
267
+ case 'rm':
268
+ return removeTasks(phases, entry, errors);
269
+ case 'append':
270
+ return appendItems(phases, entry, errors);
271
+ case 'view':
272
+ return phases;
273
+ }
274
+ }
275
+ /** Apply a single todo op to existing phases. Returns new phases + errors. */
276
+ function applyParams(phases, params) {
277
+ const errors = [];
278
+ const next = applyEntry(phases, params, errors);
279
+ normalizeInProgressTask(next);
280
+ return { phases: next, errors };
281
+ }
282
+ // =============================================================================
283
+ // Markdown round-trip
284
+ // =============================================================================
285
+ const STATUS_TO_MARKER = {
286
+ pending: ' ',
287
+ in_progress: '/',
288
+ completed: 'x',
289
+ abandoned: '-',
290
+ };
291
+ /** Render todo phases as a Markdown checklist suitable for editing/copying. */
292
+ export function phasesToMarkdown(phases) {
293
+ if (phases.length === 0)
294
+ return '# Todos\n';
295
+ const out = [];
296
+ for (let i = 0; i < phases.length; i++) {
297
+ if (i > 0)
298
+ out.push('');
299
+ const phase = phases[i];
300
+ out.push(`# ${phase.name}`);
301
+ for (const task of phase.tasks) {
302
+ out.push(`- [${STATUS_TO_MARKER[task.status]}] ${task.content}`);
303
+ }
304
+ }
305
+ return `${out.join('\n')}\n`;
306
+ }
307
+ const MARKER_TO_STATUS = {
308
+ ' ': 'pending',
309
+ '': 'pending',
310
+ x: 'completed',
311
+ X: 'completed',
312
+ '/': 'in_progress',
313
+ '>': 'in_progress',
314
+ '-': 'abandoned',
315
+ '~': 'abandoned',
316
+ };
317
+ /** Parse a Markdown checklist back into todo phases. */
318
+ export function markdownToPhases(md) {
319
+ const errors = [];
320
+ const phases = [];
321
+ let currentPhase;
322
+ const lines = md.split(/\r?\n/);
323
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
324
+ const raw = lines[lineNum];
325
+ const trimmed = raw.trim();
326
+ if (!trimmed)
327
+ continue;
328
+ const headingMatch = /^#{1,6}\s+(.+?)\s*$/.exec(trimmed);
329
+ if (headingMatch) {
330
+ currentPhase = { name: headingMatch[1].trim(), tasks: [] };
331
+ phases.push(currentPhase);
332
+ continue;
333
+ }
334
+ const taskMatch = /^[-*+]\s*\[(.?)\]\s+(.+?)\s*$/.exec(trimmed);
335
+ if (taskMatch) {
336
+ if (!currentPhase) {
337
+ currentPhase = { name: 'Todos', tasks: [] };
338
+ phases.push(currentPhase);
339
+ }
340
+ const marker = taskMatch[1] ?? '';
341
+ const status = MARKER_TO_STATUS[marker];
342
+ if (!status) {
343
+ errors.push(`Line ${lineNum + 1}: unknown status marker "[${marker}]" (use [ ], [x], [/], [-])`);
344
+ continue;
345
+ }
346
+ currentPhase.tasks.push({ content: taskMatch[2].trim(), status });
347
+ continue;
348
+ }
349
+ errors.push(`Line ${lineNum + 1}: unrecognized syntax "${trimmed}"`);
350
+ }
351
+ normalizeInProgressTask(phases);
352
+ return { phases, errors };
353
+ }
354
+ // =============================================================================
355
+ // Summary formatter
356
+ // =============================================================================
357
+ function formatSummary(phases, errors, readOnly = false) {
358
+ const tasks = phases.flatMap((phase) => phase.tasks);
359
+ if (tasks.length === 0) {
360
+ if (errors.length > 0)
361
+ return `Errors: ${errors.join('; ')}`;
362
+ return readOnly ? 'Todo list is empty.' : 'Todo list cleared.';
363
+ }
364
+ const remainingByPhase = phases
365
+ .map((phase) => ({
366
+ name: phase.name,
367
+ tasks: phase.tasks.filter((task) => task.status === 'pending' || task.status === 'in_progress'),
368
+ }))
369
+ .filter((phase) => phase.tasks.length > 0);
370
+ const remainingTasks = remainingByPhase.flatMap((phase) => phase.tasks.map((task) => ({ ...task, phase: phase.name })));
371
+ let currentIdx = phases.findIndex((phase) => phase.tasks.some((task) => task.status === 'pending' || task.status === 'in_progress'));
372
+ if (currentIdx === -1)
373
+ currentIdx = phases.length - 1;
374
+ const current = phases[currentIdx];
375
+ const done = current.tasks.filter((task) => task.status === 'completed' || task.status === 'abandoned').length;
376
+ const lines = [];
377
+ if (errors.length > 0)
378
+ lines.push(`Errors: ${errors.join('; ')}`);
379
+ if (remainingTasks.length === 0) {
380
+ lines.push('Remaining items: none.');
381
+ }
382
+ else {
383
+ lines.push(`Remaining items (${remainingTasks.length}):`);
384
+ for (const task of remainingTasks) {
385
+ lines.push(` - ${task.content} [${task.status}] (${task.phase})`);
386
+ }
387
+ }
388
+ const closedAll = tasks.filter((task) => task.status === 'completed' || task.status === 'abandoned').length;
389
+ const workedAhead = phases.some((phase, idx) => idx > currentIdx &&
390
+ phase.tasks.some((task) => task.status === 'completed' || task.status === 'abandoned'));
391
+ lines.push(`Overall: ${closedAll}/${tasks.length} done, ${remainingTasks.length} open.`);
392
+ lines.push(`Active phase ${currentIdx + 1}/${phases.length} "${current.name}" (${done}/${current.tasks.length})${workedAhead
393
+ ? ' — earliest phase with open tasks; the in-progress pointer auto-advances to the earliest open task on each completion, so it can sit behind out-of-order work (nothing was un-completed).'
394
+ : '.'}`);
395
+ for (const phase of phases) {
396
+ lines.push(` ${phase.name}:`);
397
+ for (const task of phase.tasks) {
398
+ const checkbox = task.status === 'completed' ? '[X]' : '[ ]';
399
+ const tag = task.status === 'in_progress'
400
+ ? ' (in progress)'
401
+ : task.status === 'abandoned'
402
+ ? ' (dropped)'
403
+ : '';
404
+ lines.push(` - ${checkbox} ${task.content}${tag}`);
405
+ }
406
+ }
407
+ return lines.join('\n');
408
+ }
409
+ // =============================================================================
410
+ // Session resume: extract latest phases from messages
411
+ // =============================================================================
412
+ /** Extract the latest todo phases from stored messages (tool results).
413
+ * Scans backwards for the most recent `todo` tool result with phases metadata. */
414
+ export function getLatestTodoPhasesFromMessages(messages) {
415
+ for (let i = messages.length - 1; i >= 0; i--) {
416
+ const msg = messages[i];
417
+ if (msg.role !== 'tool')
418
+ continue;
419
+ for (let j = msg.content.length - 1; j >= 0; j--) {
420
+ const part = msg.content[j];
421
+ if (!part || part._tag !== 'tool_result')
422
+ continue;
423
+ if (part.tool !== 'todo')
424
+ continue;
425
+ const output = part.output;
426
+ if (!output || output._tag !== 'success')
427
+ continue;
428
+ const metadata = output.metadata;
429
+ if (metadata && Array.isArray(metadata.phases)) {
430
+ return clonePhases(metadata.phases);
431
+ }
432
+ }
433
+ }
434
+ return [];
435
+ }
436
+ // =============================================================================
437
+ // Schema
438
+ // =============================================================================
439
+ const todoParameters = {
440
+ type: 'object',
441
+ description: 'Apply a single todo operation',
442
+ properties: {
443
+ op: {
444
+ type: 'string',
445
+ enum: ['init', 'start', 'done', 'rm', 'drop', 'append', 'view'],
446
+ description: 'Operation to apply',
447
+ },
448
+ list: {
449
+ type: 'array',
450
+ description: 'Phased task list (init only). Each entry has a phase name and task items.',
451
+ items: {
452
+ type: 'object',
453
+ properties: {
454
+ phase: { type: 'string', description: 'Phase name' },
455
+ items: {
456
+ type: 'array',
457
+ items: { type: 'string' },
458
+ minItems: 1,
459
+ description: 'Task content strings for this phase',
460
+ },
461
+ },
462
+ required: ['phase', 'items'],
463
+ },
464
+ },
465
+ task: { type: 'string', description: 'Task content (for start/done/drop/rm)' },
466
+ phase: { type: 'string', description: 'Phase name (for done/drop/rm/append, or init flat mode)' },
467
+ items: {
468
+ type: 'array',
469
+ items: { type: 'string' },
470
+ description: 'Tasks to append (append), or flat init items (init)',
471
+ },
472
+ },
473
+ required: ['op'],
474
+ additionalProperties: false,
475
+ };
476
+ // =============================================================================
477
+ // Tool definition
478
+ // =============================================================================
479
+ /** todo tool: phased task tracking with 7 operations.
480
+ * Permission: auto (no side effects beyond session state).
481
+ * State is held in-memory via ctx.todoState hook (dependency-reversal). */
482
+ export const todoTool = {
483
+ name: 'todo',
484
+ description: 'Manage a phased task list to track progress within a session. 7 operations: init (create/replace list), start (mark in progress), done (mark completed), drop (mark abandoned), rm (remove task/phase), append (add tasks to a phase), view (read-only). Tasks auto-promote: completing a task moves the in-progress pointer to the next pending task. Pass the task content text (NOT an ID) to target a specific task.',
485
+ parameters: todoParameters,
486
+ permission: 'auto',
487
+ execute: async (input, ctx) => {
488
+ const params = input;
489
+ const todoState = ctx.todoState;
490
+ if (!todoState) {
491
+ return { _tag: 'error', error: 'Todo state not available in this context' };
492
+ }
493
+ const previousPhases = clonePhases(todoState.get());
494
+ const readOnly = params.op === 'view';
495
+ const { phases: updated, errors } = readOnly
496
+ ? { phases: previousPhases, errors: [] }
497
+ : applyParams(clonePhases(previousPhases), params);
498
+ // A batch with any error is discarded wholesale: persisting a half-applied
499
+ // batch makes the natural retry hit "already exists" for the ops that did land.
500
+ const failed = errors.length > 0;
501
+ const effective = failed ? previousPhases : updated;
502
+ const completedTasks = readOnly || failed ? [] : getCompletionTransitions(previousPhases, updated);
503
+ if (!readOnly && !failed) {
504
+ todoState.set(updated);
505
+ }
506
+ const output = formatSummary(effective, errors, readOnly);
507
+ const metadata = { phases: effective };
508
+ if (completedTasks.length > 0) {
509
+ metadata.completedTasks = completedTasks;
510
+ }
511
+ // Always return success: errors are surfaced in the output text so the
512
+ // LLM can read them and retry. This avoids skewing tool metrics (a
513
+ // "task not found" is a user error, not a tool failure) and preserves
514
+ // metadata.phases for the LLM to reference on retry.
515
+ return { _tag: 'success', output, metadata };
516
+ },
517
+ };
@@ -9,6 +9,8 @@ export { grepTool } from './builtin/grep.js';
9
9
  export { readTool } from './builtin/read.js';
10
10
  export { createDefaultURLRegistry, createFileResolver, createSkillResolver, } from './builtin/resolvers.js';
11
11
  export { taskTool } from './builtin/task.js';
12
+ export { todoTool } from './builtin/todo.js';
13
+ export type { TodoInput, TodoItem, TodoPhase, TodoStatus } from './builtin/todo.js';
12
14
  export { writeTool } from './builtin/write.js';
13
15
  export { yieldTool } from './builtin/yield.js';
14
16
  export { executeTool } from './executor.js';
@@ -8,6 +8,7 @@ export { readTool } from './builtin/read.js';
8
8
  // ── Builtin tools ───────────────────────────────────────────
9
9
  export { createDefaultURLRegistry, createFileResolver, createSkillResolver, } from './builtin/resolvers.js';
10
10
  export { taskTool } from './builtin/task.js';
11
+ export { todoTool } from './builtin/todo.js';
11
12
  export { writeTool } from './builtin/write.js';
12
13
  export { yieldTool } from './builtin/yield.js';
13
14
  export { executeTool } from './executor.js';
@@ -28,6 +29,7 @@ import { globTool } from './builtin/glob.js';
28
29
  import { grepTool } from './builtin/grep.js';
29
30
  import { readTool } from './builtin/read.js';
30
31
  import { taskTool } from './builtin/task.js';
32
+ import { todoTool } from './builtin/todo.js';
31
33
  import { writeTool } from './builtin/write.js';
32
34
  import { yieldTool } from './builtin/yield.js';
33
35
  import { createToolRegistry, registerTool } from './registry.js';
@@ -47,6 +49,7 @@ export function createDefaultRegistry(config = DEFAULT_CONFIG) {
47
49
  registerTool(reg, grepTool);
48
50
  registerTool(reg, bashTool);
49
51
  registerTool(reg, taskTool);
52
+ registerTool(reg, todoTool);
50
53
  registerTool(reg, yieldTool);
51
54
  registerTool(reg, createWebSearchTool(config.websearch));
52
55
  for (const tool of dapTools)
@@ -91,6 +91,37 @@ type BashInput = {
91
91
  timeout?: number;
92
92
  env?: Record<string, string>;
93
93
  };
94
+ /** Input for the todo tool. */
95
+ type TodoInput = {
96
+ op: 'init';
97
+ list?: {
98
+ phase: string;
99
+ items: string[];
100
+ }[];
101
+ phase?: string;
102
+ items?: string[];
103
+ } | {
104
+ op: 'start';
105
+ task: string;
106
+ } | {
107
+ op: 'done';
108
+ task?: string;
109
+ phase?: string;
110
+ } | {
111
+ op: 'drop';
112
+ task?: string;
113
+ phase?: string;
114
+ } | {
115
+ op: 'rm';
116
+ task?: string;
117
+ phase?: string;
118
+ } | {
119
+ op: 'append';
120
+ phase: string;
121
+ items: string[];
122
+ } | {
123
+ op: 'view';
124
+ };
94
125
  /** A single grep match. */
95
126
  type GrepMatch = {
96
127
  file: string;
@@ -98,4 +129,4 @@ type GrepMatch = {
98
129
  text: string;
99
130
  match: string;
100
131
  };
101
- export type { BashInput, ChatTool, EditInput, GlobInput, GrepInput, GrepMatch, JSONSchema, PermissionChecker, PermissionResult, ReadInput, ToolContext, ToolDef, ToolExecutor, ToolFactory, ToolFactoryContext, ToolMode, ToolPermission, ToolRegistry, ToolResult, TruncateOptions, TruncateResult, ValidationResult, WriteInput, };
132
+ export type { BashInput, ChatTool, EditInput, GlobInput, GrepInput, GrepMatch, JSONSchema, PermissionChecker, PermissionResult, ReadInput, TodoInput, ToolContext, ToolDef, ToolExecutor, ToolFactory, ToolFactoryContext, ToolMode, ToolPermission, ToolRegistry, ToolResult, TruncateOptions, TruncateResult, ValidationResult, WriteInput, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c0de-agent",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Open-source AI coding assistant with Browser-Server architecture",
5
5
  "type": "module",
6
6
  "bin": {
@@ -107,6 +107,7 @@
107
107
  "react-dom": "^19.2.7",
108
108
  "react-router-dom": "^7.18.0",
109
109
  "shiki": "^4.3.0",
110
+ "trash": "^10.1.1",
110
111
  "undici": "^8.5.0",
111
112
  "ws": "^8.21.0"
112
113
  }