clawdo 1.0.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.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/dist/db.js +906 -0
- package/dist/errors.js +48 -0
- package/dist/inbox.js +122 -0
- package/dist/index.js +759 -0
- package/dist/parser.js +78 -0
- package/dist/render.js +238 -0
- package/dist/sanitize.js +146 -0
- package/dist/types.js +4 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { TodoDatabase } from './db.js';
|
|
6
|
+
import { parseTaskText } from './parser.js';
|
|
7
|
+
import { generateInbox, formatInboxJSON, formatInboxMarkdown } from './inbox.js';
|
|
8
|
+
import * as readline from 'readline';
|
|
9
|
+
import { renderTaskList, renderTaskDetail, renderHistory, renderStats, renderError } from './render.js';
|
|
10
|
+
const program = new Command();
|
|
11
|
+
// Helper function for readline confirmations
|
|
12
|
+
function confirmAction(message, callback) {
|
|
13
|
+
if (!process.stdin.isTTY) {
|
|
14
|
+
// Non-interactive mode - auto-confirm
|
|
15
|
+
callback(true);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const rl = readline.createInterface({
|
|
19
|
+
input: process.stdin,
|
|
20
|
+
output: process.stdout
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
24
|
+
const confirmed = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
25
|
+
rl.close();
|
|
26
|
+
callback(confirmed);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error(`Error during confirmation: ${error.message}`);
|
|
31
|
+
rl.close();
|
|
32
|
+
callback(false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Helper to expand ~ in paths
|
|
36
|
+
function expandPath(path) {
|
|
37
|
+
if (path.startsWith('~/')) {
|
|
38
|
+
return join(homedir(), path.slice(2));
|
|
39
|
+
}
|
|
40
|
+
return path;
|
|
41
|
+
}
|
|
42
|
+
// Get DB and audit paths (allow override via env var or --db flag)
|
|
43
|
+
function getDbPaths(customDbPath) {
|
|
44
|
+
const basePath = customDbPath || process.env.CLAWDO_DB_PATH;
|
|
45
|
+
if (basePath) {
|
|
46
|
+
// Custom path specified - use it directly
|
|
47
|
+
const expanded = expandPath(basePath);
|
|
48
|
+
return {
|
|
49
|
+
dbPath: expanded,
|
|
50
|
+
auditPath: expanded.replace(/\.db$/, '.audit.jsonl')
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// Default: ~/.config/clawdo/
|
|
54
|
+
const configDir = expandPath('~/.config/clawdo');
|
|
55
|
+
return {
|
|
56
|
+
dbPath: join(configDir, 'clawdo.db'),
|
|
57
|
+
auditPath: join(configDir, 'audit.jsonl')
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Helper to get DB instance
|
|
61
|
+
function getDb(customDbPath) {
|
|
62
|
+
const { dbPath, auditPath } = getDbPaths(customDbPath);
|
|
63
|
+
return new TodoDatabase(dbPath, auditPath);
|
|
64
|
+
}
|
|
65
|
+
// Helper to resolve task ID from prefix
|
|
66
|
+
function resolveId(db, idOrPrefix) {
|
|
67
|
+
try {
|
|
68
|
+
const resolved = db.resolveTaskId(idOrPrefix);
|
|
69
|
+
if (!resolved) {
|
|
70
|
+
throw new Error(`Task not found: ${idOrPrefix}`);
|
|
71
|
+
}
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw error; // Re-throw ambiguous match errors
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Helper to resolve multiple task IDs from comma-separated list
|
|
79
|
+
function resolveIds(db, idsOrPrefixes) {
|
|
80
|
+
const parts = idsOrPrefixes.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
|
81
|
+
const resolved = [];
|
|
82
|
+
for (const part of parts) {
|
|
83
|
+
const id = resolveId(db, part);
|
|
84
|
+
resolved.push(id);
|
|
85
|
+
}
|
|
86
|
+
return resolved;
|
|
87
|
+
}
|
|
88
|
+
// Helper to truncate text
|
|
89
|
+
function truncateText(text, maxLen = 60) {
|
|
90
|
+
if (text.length <= maxLen)
|
|
91
|
+
return text;
|
|
92
|
+
return text.substring(0, maxLen - 3) + '...';
|
|
93
|
+
}
|
|
94
|
+
// Helper to format time ago
|
|
95
|
+
function formatTimeAgo(isoTimestamp) {
|
|
96
|
+
const now = new Date();
|
|
97
|
+
const then = new Date(isoTimestamp);
|
|
98
|
+
const diffMs = now.getTime() - then.getTime();
|
|
99
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
100
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
101
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
102
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
103
|
+
if (diffSec < 60)
|
|
104
|
+
return `${diffSec}s ago`;
|
|
105
|
+
if (diffMin < 60)
|
|
106
|
+
return `${diffMin}m ago`;
|
|
107
|
+
if (diffHour < 24)
|
|
108
|
+
return `${diffHour}h ago`;
|
|
109
|
+
if (diffDay < 7)
|
|
110
|
+
return `${diffDay}d ago`;
|
|
111
|
+
const diffWeek = Math.floor(diffDay / 7);
|
|
112
|
+
return `${diffWeek}w ago`;
|
|
113
|
+
}
|
|
114
|
+
program
|
|
115
|
+
.name('clawdo')
|
|
116
|
+
.description('Personal task queue with autonomous execution — claw + to-do')
|
|
117
|
+
.version('1.0.0')
|
|
118
|
+
.option('--db <path>', 'Database path (default: ~/.config/clawdo/clawdo.db, or $CLAWDO_DB_PATH)')
|
|
119
|
+
.hook('preAction', (thisCommand) => {
|
|
120
|
+
const opts = thisCommand.opts();
|
|
121
|
+
if (opts.db) {
|
|
122
|
+
process.env.CLAWDO_DB_PATH = opts.db;
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
.addHelpText('after', `
|
|
126
|
+
EXAMPLES (copy-paste these):
|
|
127
|
+
# Add a task (inline tags: +project @context, urgency: now/soon/whenever/someday)
|
|
128
|
+
clawdo add "Fix the login bug +backend @coding auto soon"
|
|
129
|
+
clawdo add "Review PR #42" --level collab --urgency now
|
|
130
|
+
|
|
131
|
+
# View tasks
|
|
132
|
+
clawdo list # all active tasks
|
|
133
|
+
clawdo list --status proposed # filter by status
|
|
134
|
+
clawdo next # highest priority task
|
|
135
|
+
clawdo list --json # JSON output for agents
|
|
136
|
+
|
|
137
|
+
# Work on tasks
|
|
138
|
+
clawdo start abc123 # mark in-progress
|
|
139
|
+
clawdo done abc123 # mark complete
|
|
140
|
+
clawdo done abc,def,ghi # complete multiple tasks
|
|
141
|
+
clawdo done # complete all in-progress tasks
|
|
142
|
+
|
|
143
|
+
# Agent interface (structured output)
|
|
144
|
+
clawdo inbox # what needs attention?
|
|
145
|
+
clawdo inbox --format json # structured for scripts
|
|
146
|
+
clawdo propose "New task idea" --level auto-notify
|
|
147
|
+
clawdo next --auto --json # get next auto task as JSON
|
|
148
|
+
|
|
149
|
+
# Manage tasks
|
|
150
|
+
clawdo show abc123 # show full task details
|
|
151
|
+
clawdo show abc123 --json # JSON output
|
|
152
|
+
clawdo edit abc123 --urgency now
|
|
153
|
+
clawdo archive abc,def # archive multiple tasks
|
|
154
|
+
clawdo note abc123 "Blocked on API access"
|
|
155
|
+
|
|
156
|
+
WORKFLOW EXAMPLES (for agents):
|
|
157
|
+
# Agent proposes task, human confirms
|
|
158
|
+
clawdo propose "Refactor auth module" --level auto
|
|
159
|
+
clawdo confirm <id>
|
|
160
|
+
|
|
161
|
+
# Agent picks next auto task
|
|
162
|
+
clawdo next --auto --json
|
|
163
|
+
clawdo start <id>
|
|
164
|
+
clawdo done <id>
|
|
165
|
+
|
|
166
|
+
# Bulk operations
|
|
167
|
+
clawdo done <id1>,<id2>,<id3>
|
|
168
|
+
clawdo archive --status done
|
|
169
|
+
|
|
170
|
+
AUTONOMY LEVELS:
|
|
171
|
+
auto 10 min max, no notify Small fixes, run tests, trivial tasks
|
|
172
|
+
auto-notify 30 min max, notify Research, refactor
|
|
173
|
+
collab unlimited, needs human Requires discussion
|
|
174
|
+
|
|
175
|
+
For full command details, run: clawdo <command> --help
|
|
176
|
+
`);
|
|
177
|
+
// Helper to normalize project/context tags (auto-prepend + or @)
|
|
178
|
+
function normalizeProject(project) {
|
|
179
|
+
if (!project)
|
|
180
|
+
return undefined;
|
|
181
|
+
return project.startsWith('+') ? project : `+${project}`;
|
|
182
|
+
}
|
|
183
|
+
function normalizeContext(context) {
|
|
184
|
+
if (!context)
|
|
185
|
+
return undefined;
|
|
186
|
+
return context.startsWith('@') ? context : `@${context}`;
|
|
187
|
+
}
|
|
188
|
+
// Add command (with inline parsing)
|
|
189
|
+
program
|
|
190
|
+
.command('add')
|
|
191
|
+
.description('Add a new task')
|
|
192
|
+
.argument('<text>', 'Task text (supports inline metadata: +project @context auto/soon/etc)')
|
|
193
|
+
.option('-l, --level <level>', 'Autonomy level (auto|auto-notify|collab)')
|
|
194
|
+
.option('-u, --urgency <urgency>', 'Urgency (now|soon|whenever|someday)')
|
|
195
|
+
.option('-p, --project <project>', 'Project tag (+ prefix optional)')
|
|
196
|
+
.option('-c, --context <context>', 'Context tag (@ prefix optional)')
|
|
197
|
+
.option('--due <date>', 'Due date (YYYY-MM-DD or tomorrow)')
|
|
198
|
+
.option('--blocked-by <id>', 'Blocked by task ID')
|
|
199
|
+
.action((text, options) => {
|
|
200
|
+
try {
|
|
201
|
+
const db = getDb();
|
|
202
|
+
// Parse inline metadata
|
|
203
|
+
const parsed = parseTaskText(text);
|
|
204
|
+
// Flags override inline parsing, normalize project/context
|
|
205
|
+
const finalText = parsed.cleanText;
|
|
206
|
+
const autonomy = options.level || parsed.autonomy || 'collab';
|
|
207
|
+
const urgency = options.urgency || parsed.urgency || 'whenever';
|
|
208
|
+
const project = normalizeProject(options.project) || parsed.project;
|
|
209
|
+
const context = normalizeContext(options.context) || parsed.context;
|
|
210
|
+
const dueDate = options.due || parsed.dueDate;
|
|
211
|
+
const id = db.createTask(finalText, 'human', {
|
|
212
|
+
autonomy,
|
|
213
|
+
urgency,
|
|
214
|
+
project,
|
|
215
|
+
context,
|
|
216
|
+
dueDate,
|
|
217
|
+
blockedBy: options.blockedBy,
|
|
218
|
+
});
|
|
219
|
+
console.log(`Added: ${id}`);
|
|
220
|
+
db.close();
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
console.error(`Error: ${error.message}`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
// List command
|
|
229
|
+
program
|
|
230
|
+
.command('list')
|
|
231
|
+
.alias('ls')
|
|
232
|
+
.description('List tasks')
|
|
233
|
+
.option('--project <project>', 'Filter by project')
|
|
234
|
+
.option('--level <level>', 'Filter by autonomy level')
|
|
235
|
+
.option('--urgency <urgency>', 'Filter by urgency')
|
|
236
|
+
.option('--status <status>', 'Filter by status')
|
|
237
|
+
.option('--blocked', 'Show only blocked tasks')
|
|
238
|
+
.option('--ready', 'Show only ready (unblocked, actionable) tasks')
|
|
239
|
+
.option('--all', 'Show all tasks including archived')
|
|
240
|
+
.option('--added-by <actor>', 'Filter by who added (human|agent)')
|
|
241
|
+
.option('--full', 'Show full task text without truncation')
|
|
242
|
+
.option('--json', 'Output as JSON')
|
|
243
|
+
.action((options) => {
|
|
244
|
+
try {
|
|
245
|
+
const db = getDb();
|
|
246
|
+
const format = options.json ? 'json' : 'text';
|
|
247
|
+
const filters = {};
|
|
248
|
+
if (options.project)
|
|
249
|
+
filters.project = normalizeProject(options.project);
|
|
250
|
+
if (options.level)
|
|
251
|
+
filters.autonomy = options.level;
|
|
252
|
+
if (options.urgency)
|
|
253
|
+
filters.urgency = options.urgency;
|
|
254
|
+
if (options.addedBy)
|
|
255
|
+
filters.addedBy = options.addedBy;
|
|
256
|
+
if (options.blocked)
|
|
257
|
+
filters.blocked = true;
|
|
258
|
+
if (options.ready)
|
|
259
|
+
filters.ready = true;
|
|
260
|
+
if (options.status) {
|
|
261
|
+
// Validate status value
|
|
262
|
+
const validStatuses = ['proposed', 'todo', 'in_progress', 'done', 'archived'];
|
|
263
|
+
if (!validStatuses.includes(options.status)) {
|
|
264
|
+
throw new Error(`Invalid status '${options.status}'. Must be one of: ${validStatuses.join(', ')}`);
|
|
265
|
+
}
|
|
266
|
+
filters.status = options.status;
|
|
267
|
+
}
|
|
268
|
+
else if (!options.all) {
|
|
269
|
+
// Default: show active tasks
|
|
270
|
+
filters.status = ['todo', 'in_progress', 'proposed'];
|
|
271
|
+
}
|
|
272
|
+
const tasks = db.listTasks(filters);
|
|
273
|
+
console.log(renderTaskList(tasks, format, { compact: !options.full }));
|
|
274
|
+
db.close();
|
|
275
|
+
process.exit(0);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
console.error(renderError(error, options.json ? 'json' : 'text'));
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
// Next command
|
|
283
|
+
program
|
|
284
|
+
.command('next')
|
|
285
|
+
.description('Show next highest-priority task')
|
|
286
|
+
.option('--auto', 'Show next auto-executable task')
|
|
287
|
+
.option('--json', 'Output as JSON')
|
|
288
|
+
.action((options) => {
|
|
289
|
+
try {
|
|
290
|
+
const db = getDb();
|
|
291
|
+
const format = options.json ? 'json' : 'text';
|
|
292
|
+
const task = db.getNextTask({ auto: options.auto });
|
|
293
|
+
console.log(renderTaskDetail(task, format));
|
|
294
|
+
db.close();
|
|
295
|
+
process.exit(0);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
console.error(renderError(error, options.json ? 'json' : 'text'));
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
// Show command - display full task details
|
|
303
|
+
program
|
|
304
|
+
.command('show')
|
|
305
|
+
.description('Show full task details')
|
|
306
|
+
.argument('<id>', 'Task ID or prefix')
|
|
307
|
+
.option('--json', 'Output as JSON')
|
|
308
|
+
.action((idOrPrefix, options) => {
|
|
309
|
+
try {
|
|
310
|
+
const db = getDb();
|
|
311
|
+
const format = options.json ? 'json' : 'text';
|
|
312
|
+
const id = resolveId(db, idOrPrefix);
|
|
313
|
+
const task = db.getTask(id);
|
|
314
|
+
console.log(renderTaskDetail(task, format));
|
|
315
|
+
db.close();
|
|
316
|
+
process.exit(0);
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
console.error(renderError(error, options.json ? 'json' : 'text'));
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
// Start command
|
|
324
|
+
program
|
|
325
|
+
.command('start')
|
|
326
|
+
.description('Mark task as in progress')
|
|
327
|
+
.argument('<id>', 'Task ID or prefix')
|
|
328
|
+
.action((idOrPrefix) => {
|
|
329
|
+
try {
|
|
330
|
+
const db = getDb();
|
|
331
|
+
const id = resolveId(db, idOrPrefix);
|
|
332
|
+
db.startTask(id, 'human');
|
|
333
|
+
console.log(`Started: ${id}`);
|
|
334
|
+
db.close();
|
|
335
|
+
process.exit(0);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
console.error(`Error: ${error.message}`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
// Done command
|
|
343
|
+
program
|
|
344
|
+
.command('done')
|
|
345
|
+
.description('Mark task(s) as completed')
|
|
346
|
+
.argument('[ids]', 'Task ID(s) or prefix(es), comma-separated (e.g., abc,def,ghi) - completes all in-progress tasks if omitted')
|
|
347
|
+
.option('--all', 'Mark all todo tasks as done')
|
|
348
|
+
.option('--project <project>', 'Mark all tasks in project as done')
|
|
349
|
+
.action((ids, options) => {
|
|
350
|
+
try {
|
|
351
|
+
const db = getDb();
|
|
352
|
+
// Bulk operations
|
|
353
|
+
if (options.all || options.project) {
|
|
354
|
+
if (ids) {
|
|
355
|
+
console.error('Error: Cannot specify task ID with --all or --project');
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
const filters = { status: ['todo', 'in_progress'] };
|
|
359
|
+
if (options.project)
|
|
360
|
+
filters.project = normalizeProject(options.project);
|
|
361
|
+
const tasks = db.listTasks(filters);
|
|
362
|
+
if (tasks.length === 0) {
|
|
363
|
+
console.log('No tasks found matching criteria.');
|
|
364
|
+
db.close();
|
|
365
|
+
process.exit(0);
|
|
366
|
+
}
|
|
367
|
+
console.log(`About to mark ${tasks.length} task(s) as done:`);
|
|
368
|
+
for (const task of tasks) {
|
|
369
|
+
console.log(` [${task.id}] ${task.text}`);
|
|
370
|
+
}
|
|
371
|
+
// Confirmation prompt
|
|
372
|
+
confirmAction('Continue?', (confirmed) => {
|
|
373
|
+
if (confirmed) {
|
|
374
|
+
const count = db.bulkComplete(filters, 'human');
|
|
375
|
+
console.log(`Marked ${count} task(s) as done.`);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
console.log('Cancelled.');
|
|
379
|
+
}
|
|
380
|
+
db.close();
|
|
381
|
+
process.exit(0);
|
|
382
|
+
});
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
// No ID specified - complete all in-progress tasks
|
|
386
|
+
if (!ids) {
|
|
387
|
+
const filters = { status: ['in_progress'] };
|
|
388
|
+
const tasks = db.listTasks(filters);
|
|
389
|
+
if (tasks.length === 0) {
|
|
390
|
+
console.log('No in-progress tasks to complete.');
|
|
391
|
+
db.close();
|
|
392
|
+
process.exit(0);
|
|
393
|
+
}
|
|
394
|
+
console.log(`About to mark ${tasks.length} in-progress task(s) as done:`);
|
|
395
|
+
for (const task of tasks) {
|
|
396
|
+
console.log(` [${task.id}] ${task.text}`);
|
|
397
|
+
}
|
|
398
|
+
confirmAction('Continue?', (confirmed) => {
|
|
399
|
+
if (confirmed) {
|
|
400
|
+
const count = db.bulkComplete(filters, 'human');
|
|
401
|
+
console.log(`Marked ${count} task(s) as done.`);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
console.log('Cancelled.');
|
|
405
|
+
}
|
|
406
|
+
db.close();
|
|
407
|
+
process.exit(0);
|
|
408
|
+
});
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
// Multiple task operation (comma-separated IDs)
|
|
412
|
+
const resolvedIds = resolveIds(db, ids);
|
|
413
|
+
for (const resolvedId of resolvedIds) {
|
|
414
|
+
db.completeTask(resolvedId, 'human');
|
|
415
|
+
console.log(`Completed: ${resolvedId}`);
|
|
416
|
+
}
|
|
417
|
+
db.close();
|
|
418
|
+
process.exit(0);
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
console.error(`Error: ${error.message}`);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
// Edit command
|
|
426
|
+
program
|
|
427
|
+
.command('edit')
|
|
428
|
+
.description('Edit task metadata (Note: autonomy level cannot be changed after creation)')
|
|
429
|
+
.argument('<id>', 'Task ID or prefix')
|
|
430
|
+
.option('--text <text>', 'Update task text')
|
|
431
|
+
.option('--urgency <urgency>', 'Update urgency')
|
|
432
|
+
.option('--project <project>', 'Update project (+ prefix optional)')
|
|
433
|
+
.option('--context <context>', 'Update context (@ prefix optional)')
|
|
434
|
+
.option('--due <date>', 'Update due date')
|
|
435
|
+
.action((idOrPrefix, options) => {
|
|
436
|
+
try {
|
|
437
|
+
const db = getDb();
|
|
438
|
+
const id = resolveId(db, idOrPrefix);
|
|
439
|
+
const updates = {};
|
|
440
|
+
if (options.text !== undefined)
|
|
441
|
+
updates.text = options.text;
|
|
442
|
+
if (options.urgency)
|
|
443
|
+
updates.urgency = options.urgency;
|
|
444
|
+
if (options.project !== undefined)
|
|
445
|
+
updates.project = normalizeProject(options.project);
|
|
446
|
+
if (options.context !== undefined)
|
|
447
|
+
updates.context = normalizeContext(options.context);
|
|
448
|
+
if (options.due !== undefined)
|
|
449
|
+
updates.dueDate = options.due;
|
|
450
|
+
db.updateTask(id, updates, 'human');
|
|
451
|
+
console.log(`Updated: ${id}`);
|
|
452
|
+
db.close();
|
|
453
|
+
process.exit(0);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
console.error(`Error: ${error.message}`);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
// Archive command
|
|
461
|
+
program
|
|
462
|
+
.command('archive')
|
|
463
|
+
.description('Archive task(s)')
|
|
464
|
+
.argument('[ids]', 'Task ID(s), comma-separated (e.g., abc,def) - optional if using --all or --status')
|
|
465
|
+
.option('--all', 'Archive all non-active tasks')
|
|
466
|
+
.option('--status <status>', 'Archive all tasks with status (e.g., done)')
|
|
467
|
+
.action((ids, options) => {
|
|
468
|
+
try {
|
|
469
|
+
const db = getDb();
|
|
470
|
+
// Bulk operations
|
|
471
|
+
if (options.all || options.status) {
|
|
472
|
+
if (ids) {
|
|
473
|
+
console.error('Error: Cannot specify task ID with --all or --status');
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
const filters = {};
|
|
477
|
+
if (options.status) {
|
|
478
|
+
filters.status = options.status;
|
|
479
|
+
}
|
|
480
|
+
else if (options.all) {
|
|
481
|
+
filters.status = ['done', 'proposed'];
|
|
482
|
+
}
|
|
483
|
+
const tasks = db.listTasks(filters);
|
|
484
|
+
if (tasks.length === 0) {
|
|
485
|
+
console.log('No tasks found matching criteria.');
|
|
486
|
+
db.close();
|
|
487
|
+
process.exit(0);
|
|
488
|
+
}
|
|
489
|
+
console.log(`About to archive ${tasks.length} task(s):`);
|
|
490
|
+
for (const task of tasks.slice(0, 10)) {
|
|
491
|
+
console.log(` [${task.id}] ${task.text}`);
|
|
492
|
+
}
|
|
493
|
+
if (tasks.length > 10) {
|
|
494
|
+
console.log(` ... and ${tasks.length - 10} more`);
|
|
495
|
+
}
|
|
496
|
+
// Confirmation prompt
|
|
497
|
+
confirmAction('Continue?', (confirmed) => {
|
|
498
|
+
if (confirmed) {
|
|
499
|
+
const count = db.bulkArchive(filters, 'human');
|
|
500
|
+
console.log(`Archived ${count} task(s).`);
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
console.log('Cancelled.');
|
|
504
|
+
}
|
|
505
|
+
db.close();
|
|
506
|
+
process.exit(0);
|
|
507
|
+
});
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Multiple task operation
|
|
511
|
+
if (!ids) {
|
|
512
|
+
console.error('Error: Task ID required (or use --all/--status)');
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
const resolvedIds = resolveIds(db, ids);
|
|
516
|
+
for (const resolvedId of resolvedIds) {
|
|
517
|
+
db.archiveTask(resolvedId, 'human');
|
|
518
|
+
console.log(`Archived: ${resolvedId}`);
|
|
519
|
+
}
|
|
520
|
+
db.close();
|
|
521
|
+
process.exit(0);
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
console.error(`Error: ${error.message}`);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
// Unarchive command
|
|
529
|
+
program
|
|
530
|
+
.command('unarchive')
|
|
531
|
+
.description('Unarchive a task')
|
|
532
|
+
.argument('<id>', 'Task ID or prefix')
|
|
533
|
+
.action((idOrPrefix) => {
|
|
534
|
+
try {
|
|
535
|
+
const db = getDb();
|
|
536
|
+
const id = resolveId(db, idOrPrefix);
|
|
537
|
+
db.unarchiveTask(id, 'human');
|
|
538
|
+
console.log(`Unarchived: ${id}`);
|
|
539
|
+
db.close();
|
|
540
|
+
process.exit(0);
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
console.error(`Error: ${error.message}`);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
// Confirm command
|
|
548
|
+
program
|
|
549
|
+
.command('confirm')
|
|
550
|
+
.description('Confirm agent-proposed task')
|
|
551
|
+
.argument('<id>', 'Task ID or prefix')
|
|
552
|
+
.action((idOrPrefix) => {
|
|
553
|
+
try {
|
|
554
|
+
const db = getDb();
|
|
555
|
+
const id = resolveId(db, idOrPrefix);
|
|
556
|
+
db.confirmTask(id, 'human');
|
|
557
|
+
console.log(`Confirmed: ${id}`);
|
|
558
|
+
db.close();
|
|
559
|
+
process.exit(0);
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
console.error(`Error: ${error.message}`);
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
// Reject command
|
|
567
|
+
program
|
|
568
|
+
.command('reject')
|
|
569
|
+
.description('Reject agent-proposed task')
|
|
570
|
+
.argument('<id>', 'Task ID or prefix')
|
|
571
|
+
.option('--reason <text>', 'Reason for rejection')
|
|
572
|
+
.action((idOrPrefix, options) => {
|
|
573
|
+
try {
|
|
574
|
+
const db = getDb();
|
|
575
|
+
const id = resolveId(db, idOrPrefix);
|
|
576
|
+
db.rejectTask(id, 'human', options.reason);
|
|
577
|
+
console.log(`Rejected: ${id}`);
|
|
578
|
+
db.close();
|
|
579
|
+
process.exit(0);
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
console.error(`Error: ${error.message}`);
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
// Block command - accepts both "clawdo block <id> <blocker>" and "clawdo block <id> by <blocker>"
|
|
587
|
+
program
|
|
588
|
+
.command('block')
|
|
589
|
+
.description('Block a task by another task')
|
|
590
|
+
.argument('<id>', 'Task ID or prefix to block')
|
|
591
|
+
.argument('<arg2>', 'Blocker ID or "by"')
|
|
592
|
+
.argument('[blocker]', 'Blocker task ID (if arg2 was "by")')
|
|
593
|
+
.action((idOrPrefix, arg2, blockerArg) => {
|
|
594
|
+
try {
|
|
595
|
+
const db = getDb();
|
|
596
|
+
const id = resolveId(db, idOrPrefix);
|
|
597
|
+
// Determine blocker ID based on syntax
|
|
598
|
+
let blockerPrefix;
|
|
599
|
+
if (arg2.toLowerCase() === 'by' && blockerArg) {
|
|
600
|
+
// Syntax: block <id> by <blocker>
|
|
601
|
+
blockerPrefix = blockerArg;
|
|
602
|
+
}
|
|
603
|
+
else if (!blockerArg) {
|
|
604
|
+
// Syntax: block <id> <blocker>
|
|
605
|
+
blockerPrefix = arg2;
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
throw new Error('Invalid syntax. Use: block <id> <blocker> OR block <id> by <blocker>');
|
|
609
|
+
}
|
|
610
|
+
const blocker = resolveId(db, blockerPrefix);
|
|
611
|
+
db.blockTask(id, blocker, 'human');
|
|
612
|
+
console.log(`Blocked ${id} by ${blocker}`);
|
|
613
|
+
db.close();
|
|
614
|
+
process.exit(0);
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
console.error(`Error: ${error.message}`);
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
// Unblock command
|
|
622
|
+
program
|
|
623
|
+
.command('unblock')
|
|
624
|
+
.description('Unblock a task')
|
|
625
|
+
.argument('<id>', 'Task ID or prefix')
|
|
626
|
+
.action((idOrPrefix) => {
|
|
627
|
+
try {
|
|
628
|
+
const db = getDb();
|
|
629
|
+
const id = resolveId(db, idOrPrefix);
|
|
630
|
+
db.unblockTask(id, 'human');
|
|
631
|
+
console.log(`Unblocked: ${id}`);
|
|
632
|
+
db.close();
|
|
633
|
+
process.exit(0);
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
console.error(`Error: ${error.message}`);
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
// Propose command (agent adds task)
|
|
641
|
+
program
|
|
642
|
+
.command('propose')
|
|
643
|
+
.description('Propose a task (agent interface)')
|
|
644
|
+
.argument('<text>', 'Task text')
|
|
645
|
+
.option('-l, --level <level>', 'Autonomy level', 'collab')
|
|
646
|
+
.option('-u, --urgency <urgency>', 'Urgency', 'whenever')
|
|
647
|
+
.option('-p, --project <project>', 'Project tag (+ prefix optional)')
|
|
648
|
+
.action((text, options) => {
|
|
649
|
+
try {
|
|
650
|
+
const db = getDb();
|
|
651
|
+
// Check proposal limits
|
|
652
|
+
const proposedCount = db.countProposed();
|
|
653
|
+
if (proposedCount >= 5) {
|
|
654
|
+
console.error('Error: Too many proposed tasks (max 5 active)');
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
const id = db.createTask(text, 'agent', {
|
|
658
|
+
autonomy: options.level,
|
|
659
|
+
urgency: options.urgency,
|
|
660
|
+
project: normalizeProject(options.project),
|
|
661
|
+
});
|
|
662
|
+
console.log(`Proposed: ${id} (awaiting confirmation)`);
|
|
663
|
+
db.close();
|
|
664
|
+
process.exit(0);
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
console.error(`Error: ${error.message}`);
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
// Note command
|
|
672
|
+
program
|
|
673
|
+
.command('note')
|
|
674
|
+
.description('Add a note to a task')
|
|
675
|
+
.argument('<id>', 'Task ID or prefix')
|
|
676
|
+
.argument('<text>', 'Note text')
|
|
677
|
+
.action((idOrPrefix, text) => {
|
|
678
|
+
try {
|
|
679
|
+
const db = getDb();
|
|
680
|
+
const id = resolveId(db, idOrPrefix);
|
|
681
|
+
db.addNote(id, text, 'human');
|
|
682
|
+
console.log(`Note added to ${id}`);
|
|
683
|
+
db.close();
|
|
684
|
+
process.exit(0);
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
console.error(`Error: ${error.message}`);
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
// Inbox command (agent interface)
|
|
692
|
+
program
|
|
693
|
+
.command('inbox')
|
|
694
|
+
.description('Show inbox for agent (structured JSON or markdown)')
|
|
695
|
+
.option('--format <format>', 'Output format (json|markdown)', 'auto')
|
|
696
|
+
.action((options) => {
|
|
697
|
+
try {
|
|
698
|
+
const db = getDb();
|
|
699
|
+
const inbox = generateInbox(db);
|
|
700
|
+
// Auto-detect format
|
|
701
|
+
let format = options.format;
|
|
702
|
+
if (format === 'auto') {
|
|
703
|
+
format = process.stdout.isTTY ? 'markdown' : 'json';
|
|
704
|
+
}
|
|
705
|
+
if (format === 'json') {
|
|
706
|
+
console.log(formatInboxJSON(inbox));
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
console.log(formatInboxMarkdown(inbox));
|
|
710
|
+
}
|
|
711
|
+
db.close();
|
|
712
|
+
process.exit(0);
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
console.error(`Error: ${error.message}`);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
// Stats command
|
|
720
|
+
program
|
|
721
|
+
.command('stats')
|
|
722
|
+
.description('Show task statistics')
|
|
723
|
+
.option('--json', 'Output as JSON')
|
|
724
|
+
.action((options) => {
|
|
725
|
+
try {
|
|
726
|
+
const db = getDb();
|
|
727
|
+
const format = options.json ? 'json' : 'text';
|
|
728
|
+
const stats = db.getStats();
|
|
729
|
+
console.log(renderStats(stats, format));
|
|
730
|
+
db.close();
|
|
731
|
+
process.exit(0);
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
console.error(renderError(error, options.json ? 'json' : 'text'));
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
// History command
|
|
739
|
+
program
|
|
740
|
+
.command('history')
|
|
741
|
+
.description('Show task history')
|
|
742
|
+
.argument('<id>', 'Task ID or prefix')
|
|
743
|
+
.option('--json', 'Output as JSON')
|
|
744
|
+
.action((idOrPrefix, options) => {
|
|
745
|
+
try {
|
|
746
|
+
const db = getDb();
|
|
747
|
+
const format = options.json ? 'json' : 'text';
|
|
748
|
+
const id = resolveId(db, idOrPrefix);
|
|
749
|
+
const history = db.getHistory(id);
|
|
750
|
+
console.log(renderHistory(history, format));
|
|
751
|
+
db.close();
|
|
752
|
+
process.exit(0);
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
console.error(renderError(error, options.json ? 'json' : 'text'));
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
program.parse();
|