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/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();