@zerone-agent/open-agent-sdk 0.5.8 → 0.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 (47) hide show
  1. package/dist/agent.d.ts.map +1 -1
  2. package/dist/agent.js +6 -0
  3. package/dist/agent.js.map +1 -1
  4. package/dist/engine.d.ts +19 -1
  5. package/dist/engine.d.ts.map +1 -1
  6. package/dist/engine.js +89 -2
  7. package/dist/engine.js.map +1 -1
  8. package/dist/index.d.ts +10 -4
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +11 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/providers/types.d.ts +7 -0
  13. package/dist/providers/types.d.ts.map +1 -1
  14. package/dist/revert/index.d.ts +63 -0
  15. package/dist/revert/index.d.ts.map +1 -0
  16. package/dist/revert/index.js +125 -0
  17. package/dist/revert/index.js.map +1 -0
  18. package/dist/session.d.ts +20 -1
  19. package/dist/session.d.ts.map +1 -1
  20. package/dist/session.js +40 -6
  21. package/dist/session.js.map +1 -1
  22. package/dist/snapshot/git-detector.d.ts +9 -0
  23. package/dist/snapshot/git-detector.d.ts.map +1 -0
  24. package/dist/snapshot/git-detector.js +26 -0
  25. package/dist/snapshot/git-detector.js.map +1 -0
  26. package/dist/snapshot/index.d.ts +76 -0
  27. package/dist/snapshot/index.d.ts.map +1 -0
  28. package/dist/snapshot/index.js +288 -0
  29. package/dist/snapshot/index.js.map +1 -0
  30. package/dist/snapshot/semaphore.d.ts +11 -0
  31. package/dist/snapshot/semaphore.d.ts.map +1 -0
  32. package/dist/snapshot/semaphore.js +27 -0
  33. package/dist/snapshot/semaphore.js.map +1 -0
  34. package/dist/tools/todo-tool.d.ts +8 -18
  35. package/dist/tools/todo-tool.d.ts.map +1 -1
  36. package/dist/tools/todo-tool.js +138 -76
  37. package/dist/tools/todo-tool.js.map +1 -1
  38. package/dist/tools/todowrite.txt +25 -0
  39. package/dist/types.d.ts +16 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/utils/compact.d.ts +5 -0
  42. package/dist/utils/compact.d.ts.map +1 -1
  43. package/dist/utils/compact.js +30 -0
  44. package/dist/utils/compact.js.map +1 -1
  45. package/dist/utils/messages.js +2 -2
  46. package/dist/utils/messages.js.map +1 -1
  47. package/package.json +1 -1
@@ -0,0 +1,288 @@
1
+ import { execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { createHash } from 'crypto';
4
+ import { homedir } from 'os';
5
+ import { mkdir, rm, readFile, writeFile, stat } from 'fs/promises';
6
+ import { join } from 'path';
7
+ import { Semaphore } from './semaphore.js';
8
+ const execFileAsync = promisify(execFile);
9
+ const LARGE_FILE_LIMIT = 2 * 1024 * 1024; // 2MB
10
+ /**
11
+ * Compute the default snapshot directory for a worktree.
12
+ *
13
+ * Layout: ~/.openagent/snapshot/<project-hash>/<worktree-hash>/
14
+ *
15
+ * - project-hash: groups all worktrees of the same project together.
16
+ * Derived from git remote URL, or directory path as fallback.
17
+ * - worktree-hash: identifies the specific worktree path.
18
+ */
19
+ async function defaultSnapshotDir(worktree) {
20
+ const projectHash = await computeProjectHash(worktree);
21
+ const worktreeHash = createHash('sha256').update(worktree).digest('hex').slice(0, 16);
22
+ return join(homedir(), '.openagent', 'snapshot', projectHash, worktreeHash);
23
+ }
24
+ /**
25
+ * Derive a project-level hash from a worktree path.
26
+ *
27
+ * Strategy:
28
+ * 1. If git repo with remote → hash the remote URL (same project across clones/worktrees)
29
+ * 2. Otherwise → hash the absolute directory path
30
+ */
31
+ async function computeProjectHash(worktree) {
32
+ try {
33
+ const { stdout } = await execFileAsync('git', ['remote', 'get-url', 'origin'], {
34
+ cwd: worktree,
35
+ env: { ...process.env },
36
+ });
37
+ const remote = stdout.trim();
38
+ if (remote) {
39
+ return createHash('sha256').update(remote).digest('hex').slice(0, 16);
40
+ }
41
+ }
42
+ catch {
43
+ // not a git repo or no remote — fall through
44
+ }
45
+ // Fallback: hash the directory path (without trailing slash)
46
+ return createHash('sha256').update(worktree.replace(/\/$/, '')).digest('hex').slice(0, 16);
47
+ }
48
+ export class SnapshotEngine {
49
+ worktree;
50
+ _gitDir;
51
+ lock = new Semaphore();
52
+ gcTimer;
53
+ gcInterval;
54
+ constructor(opts) {
55
+ this.worktree = opts.worktree;
56
+ this._gitDir = opts.snapshotDir;
57
+ }
58
+ /** Snapshot repo path. Available after init(). */
59
+ get gitDir() {
60
+ if (!this._gitDir)
61
+ throw new Error('SnapshotEngine not initialized — call init() first');
62
+ return this._gitDir;
63
+ }
64
+ /**
65
+ * Initialize the snapshot git repo.
66
+ * Creates a bare repo and syncs ignore rules from the source repo.
67
+ */
68
+ async init() {
69
+ // Resolve default path if not explicitly provided
70
+ if (!this._gitDir) {
71
+ this._gitDir = await defaultSnapshotDir(this.worktree);
72
+ }
73
+ await this.lock.acquire();
74
+ try {
75
+ await mkdir(this.gitDir, { recursive: true });
76
+ await this.execRaw(['init', '--bare', this.gitDir]);
77
+ await this.syncIgnoreRules();
78
+ }
79
+ finally {
80
+ this.lock.release();
81
+ }
82
+ this.startGc();
83
+ }
84
+ /**
85
+ * Run a git command with GIT_DIR and GIT_WORK_TREE set.
86
+ */
87
+ async exec(args) {
88
+ return execFileAsync('git', args, {
89
+ env: {
90
+ ...process.env,
91
+ GIT_DIR: this.gitDir,
92
+ GIT_WORK_TREE: this.worktree,
93
+ },
94
+ maxBuffer: 50 * 1024 * 1024,
95
+ });
96
+ }
97
+ /**
98
+ * Run a git command without GIT_DIR/GIT_WORK_TREE (for init).
99
+ */
100
+ async execRaw(args, cwd) {
101
+ return execFileAsync('git', args, {
102
+ env: { ...process.env },
103
+ maxBuffer: 50 * 1024 * 1024,
104
+ cwd,
105
+ });
106
+ }
107
+ /**
108
+ * Sync ignore rules from the source repo's .git/info/exclude.
109
+ */
110
+ async syncIgnoreRules() {
111
+ try {
112
+ const sourceExclude = join(this.worktree, '.git', 'info', 'exclude');
113
+ const content = await readFile(sourceExclude, 'utf-8');
114
+ const targetExclude = join(this.gitDir, 'info', 'exclude');
115
+ await mkdir(join(this.gitDir, 'info'), { recursive: true });
116
+ await writeFile(targetExclude, content, 'utf-8');
117
+ }
118
+ catch {
119
+ // no source exclude — skip
120
+ }
121
+ }
122
+ /**
123
+ * Snapshot the current workspace state.
124
+ * Returns a tree hash that can be used to restore or revert later.
125
+ */
126
+ async track() {
127
+ await this.lock.acquire();
128
+ try {
129
+ await this.filterLargeUntracked();
130
+ await this.exec(['add', '--all']);
131
+ const { stdout } = await this.exec(['write-tree']);
132
+ return stdout.trim();
133
+ }
134
+ finally {
135
+ this.lock.release();
136
+ }
137
+ }
138
+ /**
139
+ * Remove large untracked files from the index and add them to exclude.
140
+ */
141
+ async filterLargeUntracked() {
142
+ try {
143
+ const { stdout } = await this.exec(['status', '--porcelain', '--untracked-files=all']);
144
+ const excludeAdditions = [];
145
+ for (const line of stdout.split('\n')) {
146
+ if (!line.startsWith('??'))
147
+ continue;
148
+ const filePath = line.slice(3).trim().replace(/"/g, '');
149
+ if (!filePath)
150
+ continue;
151
+ try {
152
+ const full = join(this.worktree, filePath);
153
+ const s = await stat(full);
154
+ if (s.size > LARGE_FILE_LIMIT) {
155
+ excludeAdditions.push(filePath);
156
+ }
157
+ }
158
+ catch {
159
+ // stat failed — skip
160
+ }
161
+ }
162
+ if (excludeAdditions.length > 0) {
163
+ const targetExclude = join(this.gitDir, 'info', 'exclude');
164
+ const existing = await readFile(targetExclude, 'utf-8').catch(() => '');
165
+ const additions = excludeAdditions
166
+ .filter((p) => !existing.includes(p))
167
+ .join('\n');
168
+ if (additions) {
169
+ await writeFile(targetExclude, existing + '\n' + additions + '\n', 'utf-8');
170
+ }
171
+ }
172
+ }
173
+ catch {
174
+ // non-fatal
175
+ }
176
+ }
177
+ /**
178
+ * Restore the entire workspace to a snapshot state.
179
+ */
180
+ async restore(hash) {
181
+ await this.lock.acquire();
182
+ try {
183
+ await this.exec(['read-tree', hash]);
184
+ await this.exec(['checkout-index', '-a', '-f']);
185
+ }
186
+ finally {
187
+ this.lock.release();
188
+ }
189
+ }
190
+ /**
191
+ * List files changed since the given snapshot hash.
192
+ */
193
+ async patchList(fromHash) {
194
+ await this.lock.acquire();
195
+ try {
196
+ await this.filterLargeUntracked();
197
+ await this.exec(['add', '--all']);
198
+ const { stdout } = await this.exec(['diff', '--cached', '--name-only', fromHash]);
199
+ return stdout.trim().split('\n').filter(Boolean);
200
+ }
201
+ finally {
202
+ this.lock.release();
203
+ }
204
+ }
205
+ /**
206
+ * Generate a unified diff between a snapshot and the current workspace.
207
+ */
208
+ async diff(fromHash) {
209
+ await this.lock.acquire();
210
+ try {
211
+ await this.filterLargeUntracked();
212
+ await this.exec(['add', '--all']);
213
+ const { stdout } = await this.exec(['diff', '--cached', fromHash]);
214
+ return stdout;
215
+ }
216
+ finally {
217
+ this.lock.release();
218
+ }
219
+ }
220
+ /**
221
+ * Revert specific files to their state at a given snapshot.
222
+ * Files that did not exist in the snapshot are deleted.
223
+ */
224
+ async revert(entries) {
225
+ await this.lock.acquire();
226
+ try {
227
+ for (const entry of entries) {
228
+ const fullPath = join(this.worktree, entry.path);
229
+ try {
230
+ await this.exec(['ls-tree', entry.hash, '--', entry.path]);
231
+ await this.exec(['checkout', entry.hash, '--', entry.path]);
232
+ }
233
+ catch {
234
+ try {
235
+ await rm(fullPath, { recursive: true, force: true });
236
+ }
237
+ catch {
238
+ // already gone — fine
239
+ }
240
+ }
241
+ }
242
+ }
243
+ finally {
244
+ this.lock.release();
245
+ }
246
+ }
247
+ /**
248
+ * Run git gc to prune unreachable objects older than 7 days.
249
+ */
250
+ async gc() {
251
+ await this.lock.acquire();
252
+ try {
253
+ await this.exec(['gc', '--prune=7.days', '--quiet']);
254
+ }
255
+ catch {
256
+ // non-fatal — gc is best-effort
257
+ }
258
+ finally {
259
+ this.lock.release();
260
+ }
261
+ }
262
+ /**
263
+ * Start periodic background gc.
264
+ * First run after 1 minute, then every hour.
265
+ */
266
+ startGc() {
267
+ this.gcTimer = setTimeout(() => {
268
+ this.gc();
269
+ this.gcInterval = setInterval(() => {
270
+ this.gc();
271
+ }, 60 * 60 * 1000);
272
+ }, 60 * 1000);
273
+ }
274
+ /**
275
+ * Stop background gc. Call on engine shutdown or in test cleanup.
276
+ */
277
+ stopGc() {
278
+ if (this.gcTimer) {
279
+ clearTimeout(this.gcTimer);
280
+ this.gcTimer = undefined;
281
+ }
282
+ if (this.gcInterval) {
283
+ clearInterval(this.gcInterval);
284
+ this.gcInterval = undefined;
285
+ }
286
+ }
287
+ }
288
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/snapshot/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Simple promise-based serial lock.
3
+ * Ensures git operations on the same repo are serialized.
4
+ */
5
+ export declare class Semaphore {
6
+ private queue;
7
+ private locked;
8
+ acquire(): Promise<void>;
9
+ release(): void;
10
+ }
11
+ //# sourceMappingURL=semaphore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semaphore.d.ts","sourceRoot":"","sources":["../../src/snapshot/semaphore.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,MAAM,CAAQ;IAEhB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAU9B,OAAO,IAAI,IAAI;CAQhB"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Simple promise-based serial lock.
3
+ * Ensures git operations on the same repo are serialized.
4
+ */
5
+ export class Semaphore {
6
+ queue = [];
7
+ locked = false;
8
+ async acquire() {
9
+ if (!this.locked) {
10
+ this.locked = true;
11
+ return;
12
+ }
13
+ return new Promise((resolve) => {
14
+ this.queue.push(resolve);
15
+ });
16
+ }
17
+ release() {
18
+ const next = this.queue.shift();
19
+ if (next) {
20
+ next();
21
+ }
22
+ else {
23
+ this.locked = false;
24
+ }
25
+ }
26
+ }
27
+ //# sourceMappingURL=semaphore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semaphore.js","sourceRoot":"","sources":["../../src/snapshot/semaphore.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,OAAO,SAAS;IACZ,KAAK,GAAmB,EAAE,CAAA;IAC1B,MAAM,GAAG,KAAK,CAAA;IAEtB,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;YAClB,OAAM;QACR,CAAC;QACD,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YACnC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAC1B,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,OAAO;QACL,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QAC/B,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,EAAE,CAAA;QACR,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACrB,CAAC;IACH,CAAC;CACF"}
@@ -1,22 +1,12 @@
1
- /**
2
- * TodoWriteTool - Session todo/checklist management
3
- *
4
- * Manages a session-scoped todo list for tracking work items.
5
- */
6
1
  import type { ToolDefinition } from '../types.js';
7
- export interface TodoItem {
8
- id: number;
9
- text: string;
10
- done: boolean;
11
- priority?: 'high' | 'medium' | 'low';
2
+ export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
3
+ export type TodoPriority = 'high' | 'medium' | 'low';
4
+ export interface TodoInfo {
5
+ content: string;
6
+ status: TodoStatus;
7
+ priority: TodoPriority;
12
8
  }
13
- /**
14
- * Get all todos.
15
- */
16
- export declare function getTodos(): TodoItem[];
17
- /**
18
- * Clear all todos.
19
- */
20
- export declare function clearTodos(): void;
9
+ export declare function getTodos(sessionId: string): Promise<TodoInfo[]>;
10
+ export declare function clearTodos(sessionId: string): Promise<void>;
21
11
  export declare const TodoWriteTool: ToolDefinition;
22
12
  //# sourceMappingURL=todo-tool.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"todo-tool.d.ts","sourceRoot":"","sources":["../../src/tools/todo-tool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,aAAa,CAAA;AAE7D,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAA;CACrC;AAKD;;GAEG;AACH,wBAAgB,QAAQ,IAAI,QAAQ,EAAE,CAErC;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAGjC;AAED,eAAO,MAAM,aAAa,EAAE,cA8E3B,CAAA"}
1
+ {"version":3,"file":"todo-tool.d.ts","sourceRoot":"","sources":["../../src/tools/todo-tool.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,aAAa,CAAA;AAW1E,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,WAAW,CAAA;AAC9E,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAA;AAEpD,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,UAAU,CAAA;IAClB,QAAQ,EAAE,YAAY,CAAA;CACvB;AA6FD,wBAAsB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAErE;AAED,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEjE;AAED,eAAO,MAAM,aAAa,EAAE,cAqE3B,CAAA"}
@@ -1,93 +1,155 @@
1
- /**
2
- * TodoWriteTool - Session todo/checklist management
3
- *
4
- * Manages a session-scoped todo list for tracking work items.
5
- */
6
- const todoList = [];
7
- let todoCounter = 0;
8
- /**
9
- * Get all todos.
10
- */
11
- export function getTodos() {
12
- return [...todoList];
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ let _description;
7
+ try {
8
+ _description = readFileSync(join(__dirname, 'todowrite.txt'), 'utf-8');
13
9
  }
14
- /**
15
- * Clear all todos.
16
- */
17
- export function clearTodos() {
18
- todoList.length = 0;
19
- todoCounter = 0;
10
+ catch {
11
+ _description = 'Manage a structured task list for your current coding session.';
12
+ }
13
+ const VALID_STATUSES = ['pending', 'in_progress', 'completed', 'cancelled'];
14
+ const VALID_PRIORITIES = ['high', 'medium', 'low'];
15
+ function getTodosDir() {
16
+ const home = process.env.HOME || process.env.USERPROFILE || '/tmp';
17
+ return join(home, '.openagent', 'sessions');
18
+ }
19
+ function getTodosPath(sessionId) {
20
+ return join(getTodosDir(), sessionId, 'todos.json');
21
+ }
22
+ function validateSessionId(sessionId) {
23
+ if (!/^[a-zA-Z0-9_-]+$/.test(sessionId)) {
24
+ throw new Error(`Invalid sessionId: ${sessionId}. Must match /^[a-zA-Z0-9_-]+$/`);
25
+ }
26
+ return sessionId;
27
+ }
28
+ async function saveTodos(sessionId, todos) {
29
+ validateSessionId(sessionId);
30
+ const dir = join(getTodosDir(), sessionId);
31
+ await mkdir(dir, { recursive: true });
32
+ const data = {
33
+ updatedAt: new Date().toISOString(),
34
+ todos,
35
+ };
36
+ await writeFile(getTodosPath(sessionId), JSON.stringify(data, null, 2), 'utf-8');
37
+ }
38
+ async function loadTodos(sessionId) {
39
+ validateSessionId(sessionId);
40
+ try {
41
+ const raw = await readFile(getTodosPath(sessionId), 'utf-8');
42
+ const data = JSON.parse(raw);
43
+ return data.todos || [];
44
+ }
45
+ catch {
46
+ return [];
47
+ }
48
+ }
49
+ function validateTodos(todos) {
50
+ if (!Array.isArray(todos))
51
+ return 'todos must be an array';
52
+ for (let i = 0; i < todos.length; i++) {
53
+ const item = todos[i];
54
+ if (!item.content || typeof item.content !== 'string' || item.content.trim() === '') {
55
+ return `todos[${i}].content must be a non-empty string`;
56
+ }
57
+ if (!VALID_STATUSES.includes(item.status)) {
58
+ return `todos[${i}].status must be one of: ${VALID_STATUSES.join(', ')}`;
59
+ }
60
+ if (!VALID_PRIORITIES.includes(item.priority)) {
61
+ return `todos[${i}].priority must be one of: ${VALID_PRIORITIES.join(', ')}`;
62
+ }
63
+ }
64
+ const inProgressCount = todos.filter((t) => t.status === 'in_progress').length;
65
+ if (inProgressCount > 1) {
66
+ return `Warning: ${inProgressCount} tasks are in_progress. Only one should be in_progress at a time.`;
67
+ }
68
+ return null;
69
+ }
70
+ const STATUS_MARKS = {
71
+ pending: ' ',
72
+ in_progress: '•',
73
+ completed: '✓',
74
+ cancelled: '✗',
75
+ };
76
+ function formatTodos(todos) {
77
+ if (todos.length === 0)
78
+ return 'No todos.';
79
+ const lines = [];
80
+ for (const t of todos) {
81
+ lines.push(`[${STATUS_MARKS[t.status]}] ${t.content}`);
82
+ }
83
+ return lines.join('\n');
84
+ }
85
+ export async function getTodos(sessionId) {
86
+ return loadTodos(sessionId);
87
+ }
88
+ export async function clearTodos(sessionId) {
89
+ await saveTodos(sessionId, []);
20
90
  }
21
91
  export const TodoWriteTool = {
22
92
  name: 'TodoWrite',
23
- description: 'Manage a session todo/checklist. Supports add, toggle, remove, and list operations.',
93
+ description: _description,
24
94
  inputSchema: {
25
95
  type: 'object',
26
96
  properties: {
27
- action: {
28
- type: 'string',
29
- enum: ['add', 'toggle', 'remove', 'list', 'clear'],
30
- description: 'Operation to perform',
31
- },
32
- text: { type: 'string', description: 'Todo item text (for add)' },
33
- id: { type: 'number', description: 'Todo item ID (for toggle/remove)' },
34
- priority: {
35
- type: 'string',
36
- enum: ['high', 'medium', 'low'],
37
- description: 'Priority level (for add)',
97
+ todos: {
98
+ type: 'array',
99
+ description: 'The updated todo list',
100
+ items: {
101
+ type: 'object',
102
+ properties: {
103
+ content: { type: 'string', description: 'Brief description of the task' },
104
+ status: {
105
+ type: 'string',
106
+ enum: ['pending', 'in_progress', 'completed', 'cancelled'],
107
+ description: 'Current status of the task',
108
+ },
109
+ priority: {
110
+ type: 'string',
111
+ enum: ['high', 'medium', 'low'],
112
+ description: 'Priority level of the task',
113
+ },
114
+ },
115
+ required: ['content', 'status', 'priority'],
116
+ },
38
117
  },
39
118
  },
40
- required: ['action'],
119
+ required: ['todos'],
41
120
  },
42
121
  isReadOnly: () => false,
43
122
  isConcurrencySafe: () => true,
44
123
  isEnabled: () => true,
45
- async prompt() { return 'Manage session todo list.'; },
46
- async call(input) {
47
- switch (input.action) {
48
- case 'add': {
49
- if (!input.text) {
50
- return { type: 'tool_result', tool_use_id: '', content: 'text required', is_error: true };
51
- }
52
- const item = {
53
- id: ++todoCounter,
54
- text: input.text,
55
- done: false,
56
- priority: input.priority,
57
- };
58
- todoList.push(item);
59
- return { type: 'tool_result', tool_use_id: '', content: `Todo added: #${item.id} "${item.text}"` };
60
- }
61
- case 'toggle': {
62
- const item = todoList.find(t => t.id === input.id);
63
- if (!item) {
64
- return { type: 'tool_result', tool_use_id: '', content: `Todo #${input.id} not found`, is_error: true };
65
- }
66
- item.done = !item.done;
67
- return { type: 'tool_result', tool_use_id: '', content: `Todo #${item.id} ${item.done ? 'completed' : 'reopened'}` };
68
- }
69
- case 'remove': {
70
- const idx = todoList.findIndex(t => t.id === input.id);
71
- if (idx === -1) {
72
- return { type: 'tool_result', tool_use_id: '', content: `Todo #${input.id} not found`, is_error: true };
73
- }
74
- todoList.splice(idx, 1);
75
- return { type: 'tool_result', tool_use_id: '', content: `Todo #${input.id} removed` };
76
- }
77
- case 'list': {
78
- if (todoList.length === 0) {
79
- return { type: 'tool_result', tool_use_id: '', content: 'No todos.' };
80
- }
81
- const lines = todoList.map(t => `${t.done ? '[x]' : '[ ]'} #${t.id} ${t.text}${t.priority ? ` (${t.priority})` : ''}`);
82
- return { type: 'tool_result', tool_use_id: '', content: lines.join('\n') };
83
- }
84
- case 'clear': {
85
- todoList.length = 0;
86
- return { type: 'tool_result', tool_use_id: '', content: 'All todos cleared.' };
87
- }
88
- default:
89
- return { type: 'tool_result', tool_use_id: '', content: `Unknown action: ${input.action}`, is_error: true };
124
+ async prompt() {
125
+ return _description;
126
+ },
127
+ async call(input, context) {
128
+ const todos = input.todos;
129
+ if (!Array.isArray(todos)) {
130
+ return { type: 'tool_result', tool_use_id: '', content: 'todos must be an array', is_error: true };
131
+ }
132
+ const validationError = validateTodos(todos);
133
+ if (validationError && validationError.startsWith('todos[')) {
134
+ return { type: 'tool_result', tool_use_id: '', content: validationError, is_error: true };
135
+ }
136
+ const sessionId = context.sessionId || 'default';
137
+ try {
138
+ validateSessionId(sessionId);
139
+ }
140
+ catch (e) {
141
+ return { type: 'tool_result', tool_use_id: '', content: e.message, is_error: true };
90
142
  }
143
+ await saveTodos(sessionId, todos);
144
+ const formatted = formatTodos(todos);
145
+ const json = JSON.stringify(todos, null, 2);
146
+ const output = `${formatted}\n\n${json}`;
147
+ const warning = validationError ? `\n\nNote: ${validationError}` : '';
148
+ return {
149
+ type: 'tool_result',
150
+ tool_use_id: '',
151
+ content: output + warning,
152
+ };
91
153
  },
92
154
  };
93
155
  //# sourceMappingURL=todo-tool.js.map