clawck 0.1.0 → 0.1.3

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/README.md CHANGED
@@ -160,15 +160,37 @@ Entries merge cleanly by UUID — no conflicts, no duplicates.
160
160
  ## CLI Commands
161
161
 
162
162
  ```bash
163
- clawck init # Create .clawck/ directory with config
164
- clawck serve # Start API + dashboard (default: port 3456)
165
- clawck serve --port 8080 # Custom port
166
- clawck mcp # Start MCP server on stdio
167
- clawck status # Show running tasks and stats
168
- clawck report # Timesheet summary (last 7 days)
169
- clawck report --days 30 # Last 30 days
170
- clawck report --client acme # Filter by client
171
- clawck seed --count 50 # Generate test data
163
+ clawck init # Create .clawck/ directory with config
164
+ clawck serve # Start API + dashboard (default: port 3456)
165
+ clawck serve --port 8080 # Custom port
166
+ clawck mcp # Start MCP server on stdio
167
+
168
+ # Time tracking
169
+ clawck start <task> # Start a timer
170
+ clawck stop <id> # Stop a timer
171
+ clawck log <task> --duration 30 # Log a completed task retroactively
172
+
173
+ # Viewing entries
174
+ clawck status # Show running tasks and stats
175
+ clawck list # List entries in a table (last 7 days)
176
+ clawck list --days 30 --project web # Filter by days/project/client/agent
177
+ clawck get <id> # Show a single entry
178
+ clawck entries --status running # Query with filters
179
+
180
+ # Reports & export
181
+ clawck report # Timesheet summary (last 7 days)
182
+ clawck report --days 30 --detailed # With individual entries
183
+ clawck export --format csv --days 30 # Export as CSV (also: json)
184
+
185
+ # Editing & deleting
186
+ clawck edit <id> --task "New name" # Edit fields (task, project, client, agent, etc.)
187
+ clawck delete <id> # Delete an entry (supports 8-char prefix)
188
+
189
+ # Testing
190
+ clawck seed --count 50 # Generate test data
191
+
192
+ # Global options
193
+ clawck --json <command> # Output as JSON (for scripting)
172
194
  ```
173
195
 
174
196
  ## Configuration
@@ -197,6 +219,8 @@ Edit `.clawck/config.json`:
197
219
  }
198
220
  ```
199
221
 
222
+ Clawck validates config on load. Invalid values (e.g., non-numeric port, malformed human_equivalents) will show a clear error message.
223
+
200
224
  ## REST API
201
225
 
202
226
  | Method | Endpoint | Description |
@@ -234,7 +258,7 @@ clawck/
234
258
  **Design principles:**
235
259
  - **Zero external dependencies** — SQLite is embedded, no Redis/Postgres/Docker needed
236
260
  - **One process** — API, dashboard, and MCP all run from the same `clawck serve`
237
- - **Append-only writes** — Entries are created and updated, never deleted
261
+ - **Append-first writes** — Entries are created and updated; deletion available via CLI
238
262
  - **UUID-based merging** — Multi-agent data combines without conflicts
239
263
  - **Configurable multipliers** — Human-equivalent estimates are transparent and adjustable
240
264
 
package/dist/cli/index.js CHANGED
@@ -26,7 +26,8 @@ const program = new commander_1.Command();
26
26
  program
27
27
  .name('clawck')
28
28
  .description('⏱️🦀 Clawck — Time tracking for AI agents')
29
- .version('0.1.0');
29
+ .version('0.1.3')
30
+ .option('--json', 'Output as JSON (for scripting/pipelines)');
30
31
  // ─── Init ─────────────────────────────────────────────────
31
32
  program
32
33
  .command('init')
@@ -34,6 +35,16 @@ program
34
35
  .option('-d, --dir <path>', 'Data directory', '.clawck')
35
36
  .action(async (opts) => {
36
37
  const dir = path_1.default.resolve(opts.dir);
38
+ // Prevent nesting: don't create .clawck inside an existing .clawck
39
+ const cwd = process.cwd();
40
+ if (path_1.default.basename(cwd) === '.clawck' && opts.dir === '.clawck') {
41
+ console.error(' Already inside a .clawck directory. Aborting to prevent nesting.');
42
+ process.exit(1);
43
+ }
44
+ if (path_1.default.basename(dir) === '.clawck' && fs_1.default.existsSync(path_1.default.join(dir, 'clawck.db'))) {
45
+ console.error(' Already inside a .clawck directory. Aborting to prevent nesting.');
46
+ process.exit(1);
47
+ }
37
48
  if (!fs_1.default.existsSync(dir)) {
38
49
  fs_1.default.mkdirSync(dir, { recursive: true });
39
50
  }
@@ -92,6 +103,11 @@ program
92
103
  const clawck = await new clawck_1.Clawck(config).ready();
93
104
  const stats = clawck.stats();
94
105
  const running = clawck.running();
106
+ if (program.opts().json) {
107
+ console.log(JSON.stringify({ stats, running }));
108
+ clawck.close();
109
+ return;
110
+ }
95
111
  console.log(`\n ⏱️🦀 Clawck Status`);
96
112
  console.log(` ├─ Total entries: ${stats.total_entries}`);
97
113
  console.log(` ├─ Running now: ${stats.running}`);
@@ -118,6 +134,7 @@ program
118
134
  .option('--client <name>', 'Filter by client')
119
135
  .option('--project <name>', 'Filter by project')
120
136
  .option('--agent <name>', 'Filter by agent')
137
+ .option('--detailed', 'Show individual entries')
121
138
  .action(async (opts) => {
122
139
  const config = loadConfig(opts.dir);
123
140
  const clawck = await new clawck_1.Clawck(config).ready();
@@ -129,10 +146,15 @@ program
129
146
  project: opts.project,
130
147
  agent: opts.agent,
131
148
  });
149
+ if (program.opts().json) {
150
+ console.log(JSON.stringify(ts));
151
+ clawck.close();
152
+ return;
153
+ }
132
154
  console.log(`\n 📋 Clawck Timesheet — Last ${days} days`);
133
155
  console.log(` ${'─'.repeat(50)}`);
134
- console.log(` ⏱️ Agent hours: ${ts.total_agent_hours.toFixed(1)} hrs`);
135
- console.log(` 👤 Human equiv: ${ts.total_human_equiv_hours.toFixed(1)} hrs`);
156
+ console.log(` ⏱️ Agent hours: ${ts.total_agent_hours.toFixed(2)} hrs`);
157
+ console.log(` 👤 Human equiv: ${ts.total_human_equiv_hours.toFixed(2)} hrs`);
136
158
  console.log(` 💰 Agent cost: $${ts.total_cost_usd.toFixed(2)}`);
137
159
  console.log(` 💚 Est. savings: $${ts.total_savings_usd.toFixed(0)}`);
138
160
  console.log(` 🔢 Total entries: ${ts.total_entries}`);
@@ -141,13 +163,206 @@ program
141
163
  console.log(`\n 📁 By Project:`);
142
164
  for (const p of ts.by_project) {
143
165
  const bar = '█'.repeat(Math.max(1, Math.round(p.agent_hours / (ts.total_agent_hours || 1) * 20)));
144
- console.log(` ${bar} ${p.project} (${p.client}): ${p.agent_hours.toFixed(1)}h → ${p.human_equiv_hours.toFixed(1)}h human equiv`);
166
+ console.log(` ${bar} ${p.project} (${p.client}): ${p.agent_hours.toFixed(2)}h → ${p.human_equiv_hours.toFixed(2)}h human equiv`);
145
167
  }
146
168
  }
147
169
  if (ts.by_agent.length > 0) {
148
170
  console.log(`\n 🤖 By Agent:`);
149
171
  for (const a of ts.by_agent) {
150
- console.log(` • ${a.agent} (${a.model}): ${a.agent_hours.toFixed(1)}h, ${a.success_rate}% success`);
172
+ console.log(` • ${a.agent} (${a.model}): ${a.agent_hours.toFixed(2)}h, ${a.success_rate}% success`);
173
+ }
174
+ }
175
+ if (opts.detailed) {
176
+ const entries = clawck.query({ client: opts.client, project: opts.project, agent: opts.agent, from, to, limit: 500 });
177
+ console.log(`\n 📝 Entries:`);
178
+ printEntryTable(entries);
179
+ }
180
+ console.log('');
181
+ clawck.close();
182
+ });
183
+ // ─── Start ───────────────────────────────────────────────
184
+ program
185
+ .command('start <task>')
186
+ .description('Start tracking time for a task')
187
+ .option('-d, --dir <path>', 'Data directory', '.clawck')
188
+ .option('--project <name>', 'Project name')
189
+ .option('--client <name>', 'Client name')
190
+ .option('--category <type>', 'Task category')
191
+ .option('--agent <name>', 'Agent name')
192
+ .option('--model <name>', 'Model name')
193
+ .option('--tags <tags...>', 'Tags')
194
+ .action(async (task, opts) => {
195
+ const config = loadConfig(opts.dir);
196
+ const clawck = await new clawck_1.Clawck(config).ready();
197
+ const entry = clawck.start({
198
+ task,
199
+ project: opts.project,
200
+ client: opts.client,
201
+ category: opts.category,
202
+ agent: opts.agent,
203
+ model: opts.model,
204
+ tags: opts.tags,
205
+ });
206
+ if (program.opts().json) {
207
+ console.log(JSON.stringify(entry));
208
+ }
209
+ else {
210
+ console.log(` Started: ${entry.task}`);
211
+ console.log(` ID: ${entry.id}`);
212
+ console.log(` Project: ${entry.project} Client: ${entry.client}`);
213
+ }
214
+ clawck.close();
215
+ });
216
+ // ─── Stop ────────────────────────────────────────────────
217
+ program
218
+ .command('stop <id>')
219
+ .description('Stop tracking time for a task')
220
+ .option('-d, --dir <path>', 'Data directory', '.clawck')
221
+ .option('--status <status>', 'Outcome (completed/failed)', 'completed')
222
+ .option('--summary <text>', 'Summary of work done')
223
+ .option('--tokens-in <n>', 'Input tokens consumed', parseFloat)
224
+ .option('--tokens-out <n>', 'Output tokens generated', parseFloat)
225
+ .option('--cost <n>', 'Cost in USD', parseFloat)
226
+ .option('--tool-calls <n>', 'Number of tool calls', parseInt)
227
+ .action(async (id, opts) => {
228
+ const config = loadConfig(opts.dir);
229
+ const clawck = await new clawck_1.Clawck(config).ready();
230
+ const entry = clawck.stop({
231
+ id,
232
+ status: opts.status,
233
+ summary: opts.summary,
234
+ tokens_in: opts.tokensIn,
235
+ tokens_out: opts.tokensOut,
236
+ cost_usd: opts.cost,
237
+ tool_calls: opts.toolCalls,
238
+ });
239
+ if (!entry) {
240
+ if (program.opts().json) {
241
+ console.log(JSON.stringify({ ok: false, error: `Entry not found: ${id}` }));
242
+ }
243
+ else {
244
+ console.error(` Entry not found: ${id}`);
245
+ }
246
+ clawck.close();
247
+ process.exit(1);
248
+ }
249
+ const duration_minutes = entry.end
250
+ ? +((new Date(entry.end).getTime() - new Date(entry.start).getTime()) / 60000).toFixed(1)
251
+ : null;
252
+ if (program.opts().json) {
253
+ console.log(JSON.stringify({ ...entry, duration_minutes }));
254
+ }
255
+ else {
256
+ console.log(` Stopped: ${entry.task}`);
257
+ console.log(` Duration: ${duration_minutes}m Status: ${entry.status}`);
258
+ }
259
+ clawck.close();
260
+ });
261
+ // ─── Log ─────────────────────────────────────────────────
262
+ program
263
+ .command('log <task>')
264
+ .description('Log a completed task retroactively')
265
+ .option('-d, --dir <path>', 'Data directory', '.clawck')
266
+ .option('--duration <minutes>', 'Duration in minutes', parseFloat)
267
+ .option('--project <name>', 'Project name')
268
+ .option('--client <name>', 'Client name')
269
+ .option('--category <type>', 'Task category')
270
+ .option('--agent <name>', 'Agent name')
271
+ .option('--model <name>', 'Model name')
272
+ .option('--summary <text>', 'Summary of work done')
273
+ .option('--tags <tags...>', 'Tags')
274
+ .action(async (task, opts) => {
275
+ if (!opts.duration) {
276
+ console.error(' --duration <minutes> is required');
277
+ process.exit(1);
278
+ }
279
+ const config = loadConfig(opts.dir);
280
+ const clawck = await new clawck_1.Clawck(config).ready();
281
+ const entry = clawck.log({
282
+ task,
283
+ duration_minutes: opts.duration,
284
+ project: opts.project,
285
+ client: opts.client,
286
+ category: opts.category,
287
+ agent: opts.agent,
288
+ model: opts.model,
289
+ summary: opts.summary,
290
+ tags: opts.tags,
291
+ });
292
+ if (program.opts().json) {
293
+ console.log(JSON.stringify(entry));
294
+ }
295
+ else {
296
+ console.log(` Logged: ${entry.task}`);
297
+ console.log(` ID: ${entry.id} Duration: ${opts.duration}m`);
298
+ }
299
+ clawck.close();
300
+ });
301
+ // ─── Get ─────────────────────────────────────────────────
302
+ program
303
+ .command('get <id>')
304
+ .description('Get a single time entry by ID')
305
+ .option('-d, --dir <path>', 'Data directory', '.clawck')
306
+ .action(async (id, opts) => {
307
+ const config = loadConfig(opts.dir);
308
+ const clawck = await new clawck_1.Clawck(config).ready();
309
+ const entry = clawck.get(id);
310
+ if (!entry) {
311
+ if (program.opts().json) {
312
+ console.log(JSON.stringify({ ok: false, error: `Entry not found: ${id}` }));
313
+ }
314
+ else {
315
+ console.error(` Entry not found: ${id}`);
316
+ }
317
+ clawck.close();
318
+ process.exit(1);
319
+ }
320
+ if (program.opts().json) {
321
+ console.log(JSON.stringify(entry));
322
+ }
323
+ else {
324
+ console.log(` ${entry.task}`);
325
+ console.log(` ID: ${entry.id} Status: ${entry.status}`);
326
+ console.log(` Project: ${entry.project} Client: ${entry.client}`);
327
+ console.log(` Agent: ${entry.agent} Model: ${entry.model}`);
328
+ console.log(` Start: ${entry.start} End: ${entry.end || '(running)'}`);
329
+ }
330
+ clawck.close();
331
+ });
332
+ // ─── Entries ─────────────────────────────────────────────
333
+ program
334
+ .command('entries')
335
+ .description('Query time entries')
336
+ .option('-d, --dir <path>', 'Data directory', '.clawck')
337
+ .option('--client <name>', 'Filter by client')
338
+ .option('--project <name>', 'Filter by project')
339
+ .option('--agent <name>', 'Filter by agent')
340
+ .option('--status <status>', 'Filter by status')
341
+ .option('--from <date>', 'Start date (ISO 8601)')
342
+ .option('--to <date>', 'End date (ISO 8601)')
343
+ .option('--limit <n>', 'Max entries', '50')
344
+ .action(async (opts) => {
345
+ const config = loadConfig(opts.dir);
346
+ const clawck = await new clawck_1.Clawck(config).ready();
347
+ const entries = clawck.query({
348
+ client: opts.client,
349
+ project: opts.project,
350
+ agent: opts.agent,
351
+ status: opts.status,
352
+ from: opts.from,
353
+ to: opts.to,
354
+ limit: parseInt(opts.limit) || 50,
355
+ });
356
+ if (program.opts().json) {
357
+ console.log(JSON.stringify(entries));
358
+ }
359
+ else {
360
+ console.log(`\n ${entries.length} entries:`);
361
+ for (const e of entries) {
362
+ const dur = e.end
363
+ ? `${((new Date(e.end).getTime() - new Date(e.start).getTime()) / 60000).toFixed(0)}m`
364
+ : 'running';
365
+ console.log(` ${e.id.slice(0, 8)} ${e.status.padEnd(9)} ${dur.padStart(6)} ${e.project}/${e.client} ${e.task.slice(0, 50)}`);
151
366
  }
152
367
  }
153
368
  console.log('');
@@ -210,7 +425,227 @@ program
210
425
  console.log(` └─ Run: clawck serve\n`);
211
426
  clawck.close();
212
427
  });
428
+ // ─── List ───────────────────────────────────────────────
429
+ program
430
+ .command('list')
431
+ .description('List time entries in a human-readable table')
432
+ .option('-d, --dir <path>', 'Data directory', '.clawck')
433
+ .option('--days <number>', 'Number of days to include', '7')
434
+ .option('--client <name>', 'Filter by client')
435
+ .option('--project <name>', 'Filter by project')
436
+ .option('--agent <name>', 'Filter by agent')
437
+ .option('--limit <n>', 'Max entries', '50')
438
+ .action(async (opts) => {
439
+ const config = loadConfig(opts.dir);
440
+ const clawck = await new clawck_1.Clawck(config).ready();
441
+ const days = parseInt(opts.days) || 7;
442
+ const from = new Date(Date.now() - days * 86400000).toISOString();
443
+ const to = new Date().toISOString();
444
+ const entries = clawck.query({
445
+ client: opts.client,
446
+ project: opts.project,
447
+ agent: opts.agent,
448
+ from,
449
+ to,
450
+ limit: parseInt(opts.limit) || 50,
451
+ });
452
+ if (program.opts().json) {
453
+ console.log(JSON.stringify(entries));
454
+ clawck.close();
455
+ return;
456
+ }
457
+ printEntryTable(entries);
458
+ clawck.close();
459
+ });
460
+ // ─── Delete ─────────────────────────────────────────────
461
+ program
462
+ .command('delete <id>')
463
+ .description('Delete a time entry by ID (supports 8-char prefix)')
464
+ .option('-d, --dir <path>', 'Data directory', '.clawck')
465
+ .action(async (id, opts) => {
466
+ const config = loadConfig(opts.dir);
467
+ const clawck = await new clawck_1.Clawck(config).ready();
468
+ const entry = resolveEntryId(clawck, id);
469
+ if (!entry) {
470
+ clawck.close();
471
+ process.exit(1);
472
+ }
473
+ clawck.delete(entry.id);
474
+ if (program.opts().json) {
475
+ console.log(JSON.stringify({ ok: true, deleted: entry }));
476
+ }
477
+ else {
478
+ console.log(` Deleted: ${entry.task}`);
479
+ console.log(` ID: ${entry.id.slice(0, 8)} Project: ${entry.project} Agent: ${entry.agent}`);
480
+ }
481
+ clawck.close();
482
+ });
483
+ // ─── Edit ───────────────────────────────────────────────
484
+ program
485
+ .command('edit <id>')
486
+ .description('Edit a time entry by ID (supports 8-char prefix)')
487
+ .option('-d, --dir <path>', 'Data directory', '.clawck')
488
+ .option('--task <text>', 'Update task description')
489
+ .option('--duration <minutes>', 'Update duration (recalculates end)', parseFloat)
490
+ .option('--project <name>', 'Update project')
491
+ .option('--client <name>', 'Update client')
492
+ .option('--agent <name>', 'Update agent')
493
+ .option('--category <type>', 'Update category')
494
+ .option('--summary <text>', 'Update summary')
495
+ .action(async (id, opts) => {
496
+ const config = loadConfig(opts.dir);
497
+ const clawck = await new clawck_1.Clawck(config).ready();
498
+ const entry = resolveEntryId(clawck, id);
499
+ if (!entry) {
500
+ clawck.close();
501
+ process.exit(1);
502
+ }
503
+ const updates = {};
504
+ if (opts.task)
505
+ updates.task = opts.task;
506
+ if (opts.project)
507
+ updates.project = opts.project;
508
+ if (opts.client)
509
+ updates.client = opts.client;
510
+ if (opts.agent)
511
+ updates.agent = opts.agent;
512
+ if (opts.category)
513
+ updates.category = opts.category;
514
+ if (opts.summary)
515
+ updates.summary = opts.summary;
516
+ if (opts.duration !== undefined) {
517
+ updates.end = new Date(new Date(entry.start).getTime() + opts.duration * 60000).toISOString();
518
+ }
519
+ const updated = clawck.update(entry.id, updates);
520
+ if (program.opts().json) {
521
+ console.log(JSON.stringify(updated));
522
+ }
523
+ else {
524
+ console.log(` Updated: ${updated.task}`);
525
+ console.log(` ID: ${updated.id.slice(0, 8)} Project: ${updated.project} Agent: ${updated.agent}`);
526
+ if (opts.duration !== undefined) {
527
+ console.log(` Duration: ${formatDuration(opts.duration)}`);
528
+ }
529
+ }
530
+ clawck.close();
531
+ });
532
+ // ─── Export ──────────────────────────────────────────────
533
+ program
534
+ .command('export')
535
+ .description('Export time entries as JSON or CSV')
536
+ .option('-d, --dir <path>', 'Data directory', '.clawck')
537
+ .option('--format <type>', 'Output format (json or csv)', 'json')
538
+ .option('--days <number>', 'Number of days to include', '7')
539
+ .option('--client <name>', 'Filter by client')
540
+ .option('--project <name>', 'Filter by project')
541
+ .option('--agent <name>', 'Filter by agent')
542
+ .action(async (opts) => {
543
+ const config = loadConfig(opts.dir);
544
+ const clawck = await new clawck_1.Clawck(config).ready();
545
+ const days = parseInt(opts.days) || 7;
546
+ const from = new Date(Date.now() - days * 86400000).toISOString();
547
+ const to = new Date().toISOString();
548
+ const entries = clawck.query({
549
+ client: opts.client,
550
+ project: opts.project,
551
+ agent: opts.agent,
552
+ from,
553
+ to,
554
+ limit: 10000,
555
+ });
556
+ if (opts.format === 'csv') {
557
+ const csvEscape = (val) => {
558
+ if (val.includes(',') || val.includes('"') || val.includes('\n')) {
559
+ return '"' + val.replace(/"/g, '""') + '"';
560
+ }
561
+ return val;
562
+ };
563
+ console.log('id,date,task,category,duration_minutes,project,client,agent,model,status,tokens_in,tokens_out,cost_usd,summary');
564
+ for (const e of entries) {
565
+ const durationMin = e.end
566
+ ? ((new Date(e.end).getTime() - new Date(e.start).getTime()) / 60000).toFixed(2)
567
+ : '0';
568
+ const date = e.start.split('T')[0];
569
+ console.log([
570
+ csvEscape(e.id),
571
+ date,
572
+ csvEscape(e.task),
573
+ csvEscape(e.category),
574
+ durationMin,
575
+ csvEscape(e.project),
576
+ csvEscape(e.client),
577
+ csvEscape(e.agent),
578
+ csvEscape(e.model),
579
+ csvEscape(e.status),
580
+ String(e.tokens_in),
581
+ String(e.tokens_out),
582
+ e.cost_usd.toFixed(4),
583
+ csvEscape(e.summary),
584
+ ].join(','));
585
+ }
586
+ }
587
+ else {
588
+ console.log(JSON.stringify(entries, null, 2));
589
+ }
590
+ clawck.close();
591
+ });
213
592
  // ─── Helpers ──────────────────────────────────────────────
593
+ function formatDuration(mins) {
594
+ if (mins < 1)
595
+ return '<1 min';
596
+ if (mins < 60)
597
+ return `${Math.round(mins)} min`;
598
+ const h = Math.floor(mins / 60);
599
+ const m = Math.round(mins % 60);
600
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
601
+ }
602
+ function resolveEntryId(clawck, idPrefix) {
603
+ const exact = clawck.get(idPrefix);
604
+ if (exact)
605
+ return exact;
606
+ const matches = clawck.findByPrefix(idPrefix);
607
+ if (matches.length === 0) {
608
+ if (program.opts().json) {
609
+ console.log(JSON.stringify({ ok: false, error: `No entry found matching: ${idPrefix}` }));
610
+ }
611
+ else {
612
+ console.error(` No entry found matching: ${idPrefix}`);
613
+ }
614
+ return null;
615
+ }
616
+ if (matches.length > 1) {
617
+ if (program.opts().json) {
618
+ console.log(JSON.stringify({ ok: false, error: 'Ambiguous ID prefix', matches: matches.map(e => ({ id: e.id, task: e.task })) }));
619
+ }
620
+ else {
621
+ console.error(` Ambiguous ID prefix "${idPrefix}". Matches:`);
622
+ for (const m of matches) {
623
+ console.error(` ${m.id.slice(0, 8)} ${m.task.slice(0, 50)}`);
624
+ }
625
+ }
626
+ return null;
627
+ }
628
+ return matches[0];
629
+ }
630
+ function printEntryTable(entries) {
631
+ if (entries.length === 0) {
632
+ console.log('\n No entries found.\n');
633
+ return;
634
+ }
635
+ const header = ` ${'ID'.padEnd(10)} ${'Task'.padEnd(42)} ${'Duration'.padEnd(10)} ${'Project'.padEnd(12)} ${'Agent'.padEnd(12)} ${'Time'}`;
636
+ console.log(`\n${header}`);
637
+ console.log(` ${'─'.repeat(header.length - 2)}`);
638
+ for (const e of entries) {
639
+ const durationMin = e.end
640
+ ? (new Date(e.end).getTime() - new Date(e.start).getTime()) / 60000
641
+ : (Date.now() - new Date(e.start).getTime()) / 60000;
642
+ const dur = e.end ? formatDuration(durationMin) : 'running';
643
+ const task = e.task.length > 40 ? e.task.slice(0, 37) + '...' : e.task;
644
+ const time = new Date(e.start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
645
+ console.log(` ${e.id.slice(0, 8).padEnd(10)} ${task.padEnd(42)} ${dur.padEnd(10)} ${e.project.slice(0, 12).padEnd(12)} ${e.agent.padEnd(12)} ${time}`);
646
+ }
647
+ console.log(`\n ${entries.length} entries\n`);
648
+ }
214
649
  function loadConfig(dir) {
215
650
  const dataDir = path_1.default.resolve(dir);
216
651
  const configPath = path_1.default.join(dataDir, 'config.json');
@@ -223,6 +658,30 @@ function loadConfig(dir) {
223
658
  // Ignore bad config
224
659
  }
225
660
  }
661
+ // Validate key config fields
662
+ if (fileConfig.port !== undefined) {
663
+ if (typeof fileConfig.port !== 'number' || fileConfig.port < 1 || fileConfig.port > 65535) {
664
+ console.error(` Config error: "port" must be a number between 1 and 65535 (got ${JSON.stringify(fileConfig.port)})`);
665
+ process.exit(1);
666
+ }
667
+ }
668
+ if (fileConfig.human_equivalents !== undefined) {
669
+ if (typeof fileConfig.human_equivalents !== 'object' || fileConfig.human_equivalents === null) {
670
+ console.error(' Config error: "human_equivalents" must be an object');
671
+ process.exit(1);
672
+ }
673
+ for (const [key, val] of Object.entries(fileConfig.human_equivalents)) {
674
+ const v = val;
675
+ if (typeof v.multiplier !== 'number' || typeof v.human_rate_usd !== 'number') {
676
+ console.error(` Config error: "human_equivalents.${key}" must have numeric "multiplier" and "human_rate_usd"`);
677
+ process.exit(1);
678
+ }
679
+ }
680
+ }
681
+ if (fileConfig.remote_sources !== undefined && !Array.isArray(fileConfig.remote_sources)) {
682
+ console.error(' Config error: "remote_sources" must be an array');
683
+ process.exit(1);
684
+ }
226
685
  return {
227
686
  ...types_1.DEFAULT_CONFIG,
228
687
  ...fileConfig,