clawck 0.1.3 → 0.3.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 (77) hide show
  1. package/README.md +214 -166
  2. package/dist/cli/index.js +490 -53
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/core/clawck.d.ts +14 -3
  5. package/dist/core/clawck.d.ts.map +1 -1
  6. package/dist/core/clawck.js +41 -14
  7. package/dist/core/clawck.js.map +1 -1
  8. package/dist/core/database.d.ts +1 -0
  9. package/dist/core/database.d.ts.map +1 -1
  10. package/dist/core/database.js +26 -7
  11. package/dist/core/database.js.map +1 -1
  12. package/dist/core/patterns.d.ts +7 -0
  13. package/dist/core/patterns.d.ts.map +1 -0
  14. package/dist/core/patterns.js +36 -0
  15. package/dist/core/patterns.js.map +1 -0
  16. package/dist/core/types.d.ts +31 -0
  17. package/dist/core/types.d.ts.map +1 -1
  18. package/dist/core/types.js.map +1 -1
  19. package/dist/core/webhooks.d.ts +22 -0
  20. package/dist/core/webhooks.d.ts.map +1 -0
  21. package/dist/core/webhooks.js +70 -0
  22. package/dist/core/webhooks.js.map +1 -0
  23. package/dist/hooks/adapters.d.ts +8 -0
  24. package/dist/hooks/adapters.d.ts.map +1 -0
  25. package/dist/hooks/adapters.js +138 -0
  26. package/dist/hooks/adapters.js.map +1 -0
  27. package/dist/hooks/handler.d.ts +9 -0
  28. package/dist/hooks/handler.d.ts.map +1 -0
  29. package/dist/hooks/handler.js +90 -0
  30. package/dist/hooks/handler.js.map +1 -0
  31. package/dist/hooks/index.d.ts +10 -0
  32. package/dist/hooks/index.d.ts.map +1 -0
  33. package/dist/hooks/index.js +23 -0
  34. package/dist/hooks/index.js.map +1 -0
  35. package/dist/hooks/install.d.ts +16 -0
  36. package/dist/hooks/install.d.ts.map +1 -0
  37. package/dist/hooks/install.js +239 -0
  38. package/dist/hooks/install.js.map +1 -0
  39. package/dist/hooks/session.d.ts +10 -0
  40. package/dist/hooks/session.d.ts.map +1 -0
  41. package/dist/hooks/session.js +72 -0
  42. package/dist/hooks/session.js.map +1 -0
  43. package/dist/hooks/stdin.d.ts +6 -0
  44. package/dist/hooks/stdin.d.ts.map +1 -0
  45. package/dist/hooks/stdin.js +38 -0
  46. package/dist/hooks/stdin.js.map +1 -0
  47. package/dist/hooks/types.d.ts +23 -0
  48. package/dist/hooks/types.d.ts.map +1 -0
  49. package/dist/hooks/types.js +7 -0
  50. package/dist/hooks/types.js.map +1 -0
  51. package/dist/reports/html.d.ts +13 -0
  52. package/dist/reports/html.d.ts.map +1 -0
  53. package/dist/reports/html.js +321 -0
  54. package/dist/reports/html.js.map +1 -0
  55. package/dist/reports/pdf.d.ts +13 -0
  56. package/dist/reports/pdf.d.ts.map +1 -0
  57. package/dist/reports/pdf.js +169 -0
  58. package/dist/reports/pdf.js.map +1 -0
  59. package/dist/server/api.d.ts.map +1 -1
  60. package/dist/server/api.js +16 -0
  61. package/dist/server/api.js.map +1 -1
  62. package/dist/server/mcp.d.ts.map +1 -1
  63. package/dist/server/mcp.js +30 -9
  64. package/dist/server/mcp.js.map +1 -1
  65. package/docs/skills/clawck-setup.md +131 -0
  66. package/docs/skills/clawck-usage.md +148 -0
  67. package/docs/snippets/claude-md.txt +24 -0
  68. package/docs/snippets/hooks-claude.json +16 -0
  69. package/docs/snippets/hooks-cline.txt +10 -0
  70. package/docs/snippets/hooks-codex.json +16 -0
  71. package/docs/snippets/hooks-cursor.json +16 -0
  72. package/docs/snippets/hooks-gemini.json +16 -0
  73. package/docs/snippets/hooks-windsurf.json +16 -0
  74. package/docs/snippets/mcp-config.json +7 -0
  75. package/docs/snippets/openclaw-agent-md.txt +18 -0
  76. package/docs/snippets/openclaw-heartbeat-md.txt +17 -0
  77. package/package.json +4 -2
package/dist/cli/index.js CHANGED
@@ -11,6 +11,39 @@
11
11
  * clawck status Show running tasks and stats
12
12
  * clawck report [--days N] Show timesheet summary
13
13
  */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
14
47
  var __importDefault = (this && this.__importDefault) || function (mod) {
15
48
  return (mod && mod.__esModule) ? mod : { "default": mod };
16
49
  };
@@ -22,22 +55,29 @@ const api_1 = require("../server/api");
22
55
  const mcp_1 = require("../server/mcp");
23
56
  const clawck_1 = require("../core/clawck");
24
57
  const types_1 = require("../core/types");
58
+ const pdf_1 = require("../reports/pdf");
59
+ const html_1 = require("../reports/html");
60
+ const patterns_1 = require("../core/patterns");
61
+ const hooks_1 = require("../hooks");
25
62
  const program = new commander_1.Command();
26
63
  program
27
64
  .name('clawck')
28
65
  .description('⏱️🦀 Clawck — Time tracking for AI agents')
29
- .version('0.1.3')
30
- .option('--json', 'Output as JSON (for scripting/pipelines)');
66
+ .version('0.3.0')
67
+ .enablePositionalOptions()
68
+ .option('--json', 'Output as JSON (for scripting/pipelines)')
69
+ .option('-d, --dir <path>', 'Data directory (also: CLAWCK_DIR env var)');
31
70
  // ─── Init ─────────────────────────────────────────────────
32
71
  program
33
72
  .command('init')
34
73
  .description('Initialize a .clawck/ directory in the current folder')
35
- .option('-d, --dir <path>', 'Data directory', '.clawck')
74
+ .option('-d, --dir <path>', 'Data directory')
36
75
  .action(async (opts) => {
37
- const dir = path_1.default.resolve(opts.dir);
76
+ const resolvedDir = resolveDataDir(opts);
77
+ const dir = path_1.default.resolve(resolvedDir);
38
78
  // Prevent nesting: don't create .clawck inside an existing .clawck
39
79
  const cwd = process.cwd();
40
- if (path_1.default.basename(cwd) === '.clawck' && opts.dir === '.clawck') {
80
+ if (path_1.default.basename(cwd) === '.clawck' && resolvedDir === '.clawck') {
41
81
  console.error(' Already inside a .clawck directory. Aborting to prevent nesting.');
42
82
  process.exit(1);
43
83
  }
@@ -59,6 +99,8 @@ program
59
99
  default_source: 'clawck',
60
100
  human_equivalents: types_1.DEFAULT_HUMAN_EQUIVALENTS,
61
101
  remote_sources: [],
102
+ patterns: patterns_1.DEFAULT_PATTERNS,
103
+ default_pattern: 'default',
62
104
  };
63
105
  fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
64
106
  }
@@ -78,9 +120,9 @@ program
78
120
  .command('serve')
79
121
  .description('Start the Clawck API server and dashboard')
80
122
  .option('-p, --port <number>', 'Port number', '3456')
81
- .option('-d, --dir <path>', 'Data directory', '.clawck')
123
+ .option('-d, --dir <path>', 'Data directory')
82
124
  .action(async (opts) => {
83
- const config = loadConfig(opts.dir);
125
+ const config = loadConfig(resolveDataDir(opts));
84
126
  config.port = parseInt(opts.port) || config.port;
85
127
  (0, api_1.startServer)(config);
86
128
  });
@@ -88,18 +130,18 @@ program
88
130
  program
89
131
  .command('mcp')
90
132
  .description('Start the Clawck MCP server (stdio, for Claude Code / Cline / Cursor)')
91
- .option('-d, --dir <path>', 'Data directory', '.clawck')
133
+ .option('-d, --dir <path>', 'Data directory')
92
134
  .action(async (opts) => {
93
- const config = loadConfig(opts.dir);
135
+ const config = loadConfig(resolveDataDir(opts));
94
136
  (0, mcp_1.startMCPServer)(config);
95
137
  });
96
138
  // ─── Status ───────────────────────────────────────────────
97
139
  program
98
140
  .command('status')
99
141
  .description('Show currently running tasks and stats')
100
- .option('-d, --dir <path>', 'Data directory', '.clawck')
142
+ .option('-d, --dir <path>', 'Data directory')
101
143
  .action(async (opts) => {
102
- const config = loadConfig(opts.dir);
144
+ const config = loadConfig(resolveDataDir(opts));
103
145
  const clawck = await new clawck_1.Clawck(config).ready();
104
146
  const stats = clawck.stats();
105
147
  const running = clawck.running();
@@ -129,14 +171,16 @@ program
129
171
  program
130
172
  .command('report')
131
173
  .description('Show a timesheet summary')
132
- .option('-d, --dir <path>', 'Data directory', '.clawck')
174
+ .option('-d, --dir <path>', 'Data directory')
133
175
  .option('--days <number>', 'Number of days to include', '7')
134
176
  .option('--client <name>', 'Filter by client')
135
177
  .option('--project <name>', 'Filter by project')
136
178
  .option('--agent <name>', 'Filter by agent')
179
+ .option('--format <type>', 'Output format (terminal, pdf, or html)', 'terminal')
180
+ .option('--output <path>', 'Output file path (for pdf format)')
137
181
  .option('--detailed', 'Show individual entries')
138
182
  .action(async (opts) => {
139
- const config = loadConfig(opts.dir);
183
+ const config = loadConfig(resolveDataDir(opts));
140
184
  const clawck = await new clawck_1.Clawck(config).ready();
141
185
  const days = parseInt(opts.days) || 7;
142
186
  const to = new Date().toISOString();
@@ -146,6 +190,30 @@ program
146
190
  project: opts.project,
147
191
  agent: opts.agent,
148
192
  });
193
+ if (opts.format === 'html') {
194
+ const today = new Date().toISOString().split('T')[0];
195
+ const outputPath = opts.output || `clawck-report-${today}.html`;
196
+ const dateRange = `${from.split('T')[0]} to ${to.split('T')[0]}`;
197
+ const rawEntries = clawck.query({ from, to, client: opts.client, project: opts.project, agent: opts.agent, limit: 10000 });
198
+ const html = (0, html_1.generateTimesheetHTML)(ts, { dateRange, clientName: opts.client, rawEntries });
199
+ fs_1.default.writeFileSync(outputPath, html);
200
+ console.log(` HTML report saved to: ${outputPath}`);
201
+ clawck.close();
202
+ return;
203
+ }
204
+ if (opts.format === 'pdf') {
205
+ const today = new Date().toISOString().split('T')[0];
206
+ const outputPath = opts.output || `clawck-report-${today}.pdf`;
207
+ const dateRange = `${from.split('T')[0]} to ${to.split('T')[0]}`;
208
+ await (0, pdf_1.generateTimesheetPDF)(ts, {
209
+ clientName: opts.client,
210
+ dateRange,
211
+ outputPath,
212
+ });
213
+ console.log(` PDF report saved to: ${outputPath}`);
214
+ clawck.close();
215
+ return;
216
+ }
149
217
  if (program.opts().json) {
150
218
  console.log(JSON.stringify(ts));
151
219
  clawck.close();
@@ -184,15 +252,16 @@ program
184
252
  program
185
253
  .command('start <task>')
186
254
  .description('Start tracking time for a task')
187
- .option('-d, --dir <path>', 'Data directory', '.clawck')
255
+ .option('-d, --dir <path>', 'Data directory')
188
256
  .option('--project <name>', 'Project name')
189
257
  .option('--client <name>', 'Client name')
190
258
  .option('--category <type>', 'Task category')
191
259
  .option('--agent <name>', 'Agent name')
192
260
  .option('--model <name>', 'Model name')
193
261
  .option('--tags <tags...>', 'Tags')
262
+ .option('--pattern <name>', 'Use a tracking pattern')
194
263
  .action(async (task, opts) => {
195
- const config = loadConfig(opts.dir);
264
+ const config = loadConfig(resolveDataDir(opts));
196
265
  const clawck = await new clawck_1.Clawck(config).ready();
197
266
  const entry = clawck.start({
198
267
  task,
@@ -202,6 +271,7 @@ program
202
271
  agent: opts.agent,
203
272
  model: opts.model,
204
273
  tags: opts.tags,
274
+ pattern: opts.pattern,
205
275
  });
206
276
  if (program.opts().json) {
207
277
  console.log(JSON.stringify(entry));
@@ -217,7 +287,7 @@ program
217
287
  program
218
288
  .command('stop <id>')
219
289
  .description('Stop tracking time for a task')
220
- .option('-d, --dir <path>', 'Data directory', '.clawck')
290
+ .option('-d, --dir <path>', 'Data directory')
221
291
  .option('--status <status>', 'Outcome (completed/failed)', 'completed')
222
292
  .option('--summary <text>', 'Summary of work done')
223
293
  .option('--tokens-in <n>', 'Input tokens consumed', parseFloat)
@@ -225,7 +295,7 @@ program
225
295
  .option('--cost <n>', 'Cost in USD', parseFloat)
226
296
  .option('--tool-calls <n>', 'Number of tool calls', parseInt)
227
297
  .action(async (id, opts) => {
228
- const config = loadConfig(opts.dir);
298
+ const config = loadConfig(resolveDataDir(opts));
229
299
  const clawck = await new clawck_1.Clawck(config).ready();
230
300
  const entry = clawck.stop({
231
301
  id,
@@ -247,14 +317,14 @@ program
247
317
  process.exit(1);
248
318
  }
249
319
  const duration_minutes = entry.end
250
- ? +((new Date(entry.end).getTime() - new Date(entry.start).getTime()) / 60000).toFixed(1)
320
+ ? (new Date(entry.end).getTime() - new Date(entry.start).getTime()) / 60000
251
321
  : null;
252
322
  if (program.opts().json) {
253
- console.log(JSON.stringify({ ...entry, duration_minutes }));
323
+ console.log(JSON.stringify({ ...entry, duration_minutes: duration_minutes ? Math.round(duration_minutes * 100) / 100 : null }));
254
324
  }
255
325
  else {
256
326
  console.log(` Stopped: ${entry.task}`);
257
- console.log(` Duration: ${duration_minutes}m Status: ${entry.status}`);
327
+ console.log(` Duration: ${duration_minutes !== null ? formatDuration(duration_minutes) : 'unknown'} Status: ${entry.status}`);
258
328
  }
259
329
  clawck.close();
260
330
  });
@@ -262,7 +332,7 @@ program
262
332
  program
263
333
  .command('log <task>')
264
334
  .description('Log a completed task retroactively')
265
- .option('-d, --dir <path>', 'Data directory', '.clawck')
335
+ .option('-d, --dir <path>', 'Data directory')
266
336
  .option('--duration <minutes>', 'Duration in minutes', parseFloat)
267
337
  .option('--project <name>', 'Project name')
268
338
  .option('--client <name>', 'Client name')
@@ -271,12 +341,13 @@ program
271
341
  .option('--model <name>', 'Model name')
272
342
  .option('--summary <text>', 'Summary of work done')
273
343
  .option('--tags <tags...>', 'Tags')
344
+ .option('--pattern <name>', 'Use a tracking pattern')
274
345
  .action(async (task, opts) => {
275
346
  if (!opts.duration) {
276
347
  console.error(' --duration <minutes> is required');
277
348
  process.exit(1);
278
349
  }
279
- const config = loadConfig(opts.dir);
350
+ const config = loadConfig(resolveDataDir(opts));
280
351
  const clawck = await new clawck_1.Clawck(config).ready();
281
352
  const entry = clawck.log({
282
353
  task,
@@ -288,6 +359,7 @@ program
288
359
  model: opts.model,
289
360
  summary: opts.summary,
290
361
  tags: opts.tags,
362
+ pattern: opts.pattern,
291
363
  });
292
364
  if (program.opts().json) {
293
365
  console.log(JSON.stringify(entry));
@@ -302,9 +374,9 @@ program
302
374
  program
303
375
  .command('get <id>')
304
376
  .description('Get a single time entry by ID')
305
- .option('-d, --dir <path>', 'Data directory', '.clawck')
377
+ .option('-d, --dir <path>', 'Data directory')
306
378
  .action(async (id, opts) => {
307
- const config = loadConfig(opts.dir);
379
+ const config = loadConfig(resolveDataDir(opts));
308
380
  const clawck = await new clawck_1.Clawck(config).ready();
309
381
  const entry = clawck.get(id);
310
382
  if (!entry) {
@@ -326,6 +398,8 @@ program
326
398
  console.log(` Project: ${entry.project} Client: ${entry.client}`);
327
399
  console.log(` Agent: ${entry.agent} Model: ${entry.model}`);
328
400
  console.log(` Start: ${entry.start} End: ${entry.end || '(running)'}`);
401
+ console.log(` Approved: ${entry.approved ? 'yes' : 'no'}`);
402
+ console.log(` Created: ${entry.created_at} Updated: ${entry.updated_at}`);
329
403
  }
330
404
  clawck.close();
331
405
  });
@@ -333,7 +407,7 @@ program
333
407
  program
334
408
  .command('entries')
335
409
  .description('Query time entries')
336
- .option('-d, --dir <path>', 'Data directory', '.clawck')
410
+ .option('-d, --dir <path>', 'Data directory')
337
411
  .option('--client <name>', 'Filter by client')
338
412
  .option('--project <name>', 'Filter by project')
339
413
  .option('--agent <name>', 'Filter by agent')
@@ -341,9 +415,12 @@ program
341
415
  .option('--from <date>', 'Start date (ISO 8601)')
342
416
  .option('--to <date>', 'End date (ISO 8601)')
343
417
  .option('--limit <n>', 'Max entries', '50')
418
+ .option('--approved', 'Show only approved entries')
419
+ .option('--unapproved', 'Show only unapproved entries')
344
420
  .action(async (opts) => {
345
- const config = loadConfig(opts.dir);
421
+ const config = loadConfig(resolveDataDir(opts));
346
422
  const clawck = await new clawck_1.Clawck(config).ready();
423
+ const approvedFilter = opts.approved ? true : opts.unapproved ? false : undefined;
347
424
  const entries = clawck.query({
348
425
  client: opts.client,
349
426
  project: opts.project,
@@ -352,6 +429,7 @@ program
352
429
  from: opts.from,
353
430
  to: opts.to,
354
431
  limit: parseInt(opts.limit) || 50,
432
+ approved: approvedFilter,
355
433
  });
356
434
  if (program.opts().json) {
357
435
  console.log(JSON.stringify(entries));
@@ -368,21 +446,195 @@ program
368
446
  console.log('');
369
447
  clawck.close();
370
448
  });
449
+ // ─── Approve ─────────────────────────────────────────────
450
+ program
451
+ .command('approve <id>')
452
+ .description('Approve a time entry (supports 8-char prefix)')
453
+ .option('-d, --dir <path>', 'Data directory')
454
+ .action(async (id, opts) => {
455
+ const config = loadConfig(resolveDataDir(opts));
456
+ const clawck = await new clawck_1.Clawck(config).ready();
457
+ const entry = resolveEntryId(clawck, id);
458
+ if (!entry) {
459
+ clawck.close();
460
+ process.exit(1);
461
+ }
462
+ const approved = clawck.approve(entry.id);
463
+ if (program.opts().json) {
464
+ console.log(JSON.stringify(approved));
465
+ }
466
+ else {
467
+ console.log(` Approved: ${approved.task}`);
468
+ console.log(` ID: ${approved.id.slice(0, 8)} Project: ${approved.project}`);
469
+ }
470
+ clawck.close();
471
+ });
472
+ // ─── Pattern ─────────────────────────────────────────────
473
+ const pattern = program
474
+ .command('pattern')
475
+ .description('Manage tracking patterns (task templates)');
476
+ pattern
477
+ .command('list')
478
+ .description('List all tracking patterns')
479
+ .option('-d, --dir <path>', 'Data directory')
480
+ .action(async (opts) => {
481
+ const config = loadConfig(resolveDataDir(opts));
482
+ const clawck = await new clawck_1.Clawck(config).ready();
483
+ const patterns = clawck.getPatterns();
484
+ if (program.opts().json) {
485
+ console.log(JSON.stringify(patterns));
486
+ clawck.close();
487
+ return;
488
+ }
489
+ console.log(`\n Tracking Patterns:`);
490
+ for (const p of patterns) {
491
+ const defaultMark = config.default_pattern === p.name ? ' (default)' : '';
492
+ console.log(` - ${p.name}${defaultMark}`);
493
+ if (p.description)
494
+ console.log(` ${p.description}`);
495
+ const fields = [];
496
+ if (p.category)
497
+ fields.push(`category: ${p.category}`);
498
+ if (p.project)
499
+ fields.push(`project: ${p.project}`);
500
+ if (p.client)
501
+ fields.push(`client: ${p.client}`);
502
+ if (p.agent)
503
+ fields.push(`agent: ${p.agent}`);
504
+ if (p.tags?.length)
505
+ fields.push(`tags: ${p.tags.join(', ')}`);
506
+ if (fields.length)
507
+ console.log(` ${fields.join(' ')}`);
508
+ }
509
+ console.log('');
510
+ clawck.close();
511
+ });
512
+ pattern
513
+ .command('add')
514
+ .description('Add a new tracking pattern')
515
+ .requiredOption('--name <name>', 'Pattern name')
516
+ .option('-d, --dir <path>', 'Data directory')
517
+ .option('--category <type>', 'Default category')
518
+ .option('--project <name>', 'Default project')
519
+ .option('--client <name>', 'Default client')
520
+ .option('--agent <name>', 'Default agent')
521
+ .option('--tags <tags...>', 'Default tags')
522
+ .option('--description <text>', 'Pattern description')
523
+ .action(async (opts) => {
524
+ const dataDir = resolveDataDir(opts);
525
+ const config = loadConfig(dataDir);
526
+ const configPath = path_1.default.join(path_1.default.resolve(dataDir), 'config.json');
527
+ const newPattern = { name: opts.name };
528
+ if (opts.description)
529
+ newPattern.description = opts.description;
530
+ if (opts.category)
531
+ newPattern.category = opts.category;
532
+ if (opts.project)
533
+ newPattern.project = opts.project;
534
+ if (opts.client)
535
+ newPattern.client = opts.client;
536
+ if (opts.agent)
537
+ newPattern.agent = opts.agent;
538
+ if (opts.tags)
539
+ newPattern.tags = opts.tags;
540
+ let fileConfig = {};
541
+ if (fs_1.default.existsSync(configPath)) {
542
+ try {
543
+ fileConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
544
+ }
545
+ catch { }
546
+ }
547
+ if (!fileConfig.patterns)
548
+ fileConfig.patterns = [...patterns_1.DEFAULT_PATTERNS];
549
+ fileConfig.patterns.push(newPattern);
550
+ fs_1.default.writeFileSync(configPath, JSON.stringify(fileConfig, null, 2));
551
+ console.log(` Added pattern: ${opts.name}`);
552
+ });
553
+ pattern
554
+ .command('use <name>')
555
+ .description('Set the default tracking pattern')
556
+ .option('-d, --dir <path>', 'Data directory')
557
+ .action(async (name, opts) => {
558
+ const dataDir = resolveDataDir(opts);
559
+ const configPath = path_1.default.join(path_1.default.resolve(dataDir), 'config.json');
560
+ let fileConfig = {};
561
+ if (fs_1.default.existsSync(configPath)) {
562
+ try {
563
+ fileConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
564
+ }
565
+ catch { }
566
+ }
567
+ fileConfig.default_pattern = name;
568
+ fs_1.default.writeFileSync(configPath, JSON.stringify(fileConfig, null, 2));
569
+ console.log(` Default pattern set to: ${name}`);
570
+ });
571
+ // ─── Setup ───────────────────────────────────────────────
572
+ program
573
+ .command('setup [target]')
574
+ .description('Output ready-to-paste config snippets for agent integration')
575
+ .action(async (target) => {
576
+ const snippetsDir = path_1.default.resolve(__dirname, '../../docs/snippets');
577
+ function readSnippet(filename) {
578
+ const filePath = path_1.default.join(snippetsDir, filename);
579
+ if (!fs_1.default.existsSync(filePath)) {
580
+ console.error(` Snippet not found: ${filePath}`);
581
+ process.exit(1);
582
+ }
583
+ return fs_1.default.readFileSync(filePath, 'utf-8');
584
+ }
585
+ if (!target) {
586
+ console.log(`
587
+ ⏱️🦀 Clawck Setup — Agent Integration
588
+
589
+ Available targets:
590
+
591
+ clawck setup claude Output CLAUDE.md time tracking snippet
592
+ clawck setup mcp Output MCP server config JSON
593
+ clawck setup openclaw Output OpenClaw snippets (AGENT.md + HEARTBEAT.md)
594
+
595
+ Paste the output into the appropriate file for your agent platform.
596
+ See docs/skills/clawck-setup.md for full integration guide.
597
+ `);
598
+ return;
599
+ }
600
+ switch (target) {
601
+ case 'claude': {
602
+ console.log('\n# Add this to your CLAUDE.md (project or ~/.claude/CLAUDE.md for global):\n');
603
+ console.log(readSnippet('claude-md.txt'));
604
+ break;
605
+ }
606
+ case 'mcp': {
607
+ console.log('\n# Add this to your mcp_servers.json (or ~/.claude/mcp_servers.json for global):\n');
608
+ console.log(readSnippet('mcp-config.json'));
609
+ break;
610
+ }
611
+ case 'openclaw': {
612
+ console.log('\n# ─── AGENT.md ─────────────────────────────────────────\n');
613
+ console.log(readSnippet('openclaw-agent-md.txt'));
614
+ console.log('\n# ─── HEARTBEAT.md ─────────────────────────────────────\n');
615
+ console.log(readSnippet('openclaw-heartbeat-md.txt'));
616
+ break;
617
+ }
618
+ default:
619
+ console.error(` Unknown target: "${target}". Run "clawck setup" to see options.`);
620
+ process.exit(1);
621
+ }
622
+ });
371
623
  // ─── Seed (for testing) ──────────────────────────────────
372
624
  program
373
625
  .command('seed')
374
626
  .description('Seed the database with sample entries (for testing)')
375
- .option('-d, --dir <path>', 'Data directory', '.clawck')
627
+ .option('-d, --dir <path>', 'Data directory')
376
628
  .option('-n, --count <number>', 'Number of entries', '25')
377
629
  .action(async (opts) => {
378
- const config = loadConfig(opts.dir);
630
+ const config = loadConfig(resolveDataDir(opts));
379
631
  const clawck = await new clawck_1.Clawck(config).ready();
380
632
  const count = parseInt(opts.count) || 25;
381
633
  const agents = ['cubi-research-01', 'cubi-writer-02', 'cubi-coder-03', 'cubi-analyst-04', 'cubi-outreach-05'];
382
634
  const models = ['claude-sonnet-4-20250514', 'claude-haiku-4-5-20251001', 'gpt-4o', 'gemini-2.0-flash'];
383
- const clients = ['acme-corp', 'globex-inc', 'initech', 'hooli'];
384
- const projects = ['website-rebuild', 'seo-content', 'grant-research', 'data-migration', 'email-campaigns'];
385
- const categories = ['research', 'content', 'code', 'data_entry', 'analysis', 'communication', 'testing', 'design'];
635
+ const clients = ['acme-corp', 'globex-inc', 'initech', 'hooli', 'umbrella-co'];
636
+ const projects = ['website-rebuild', 'seo-content', 'grant-research', 'data-migration', 'email-campaigns', 'api-v2'];
637
+ const categories = ['research', 'content', 'code', 'data_entry', 'analysis', 'communication', 'testing', 'design', 'planning', 'other'];
386
638
  const tasks = [
387
639
  'Research competitor pricing strategies',
388
640
  'Write blog post about AI automation',
@@ -399,20 +651,26 @@ program
399
651
  'Review and optimize database queries',
400
652
  'Create investor pitch deck outline',
401
653
  'Summarize recent industry news',
654
+ 'Plan sprint roadmap for Q4',
655
+ 'Coordinate team standup notes',
402
656
  ];
657
+ const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
658
+ // Ensure coverage: cycle through all clients, projects, categories, agents
403
659
  for (let i = 0; i < count; i++) {
404
- const daysAgo = Math.random() * 14;
405
660
  const durationMin = 5 + Math.random() * 120;
406
661
  const tokensIn = Math.round(1000 + Math.random() * 50000);
407
662
  const tokensOut = Math.round(500 + Math.random() * 20000);
408
- const category = categories[Math.floor(Math.random() * categories.length)];
409
- clawck.log({
410
- task: tasks[Math.floor(Math.random() * tasks.length)],
411
- project: projects[Math.floor(Math.random() * projects.length)],
412
- client: clients[Math.floor(Math.random() * clients.length)],
663
+ const category = i < categories.length ? categories[i] : pick(categories);
664
+ const client = i < clients.length ? clients[i] : pick(clients);
665
+ const project = i < projects.length ? projects[i] : pick(projects);
666
+ const agent = i < agents.length ? agents[i] : pick(agents);
667
+ const entry = clawck.log({
668
+ task: pick(tasks),
669
+ project,
670
+ client,
413
671
  category,
414
- agent: agents[Math.floor(Math.random() * agents.length)],
415
- model: models[Math.floor(Math.random() * models.length)],
672
+ agent,
673
+ model: pick(models),
416
674
  duration_minutes: Math.round(durationMin),
417
675
  tokens_in: tokensIn,
418
676
  tokens_out: tokensOut,
@@ -420,27 +678,82 @@ program
420
678
  summary: 'Auto-generated seed entry for testing',
421
679
  tags: ['seed', 'test'],
422
680
  });
681
+ // Approve ~60% of completed entries
682
+ if (Math.random() < 0.6) {
683
+ clawck.approve(entry.id);
684
+ }
685
+ }
686
+ // Add 2-3 running entries
687
+ for (let i = 0; i < 3; i++) {
688
+ clawck.start({
689
+ task: pick(tasks),
690
+ project: pick(projects),
691
+ client: pick(clients),
692
+ category: pick(categories),
693
+ agent: pick(agents),
694
+ model: pick(models),
695
+ tags: ['seed'],
696
+ });
697
+ }
698
+ // Add 2-3 failed entries via upsert
699
+ const { v4: uuid } = await Promise.resolve().then(() => __importStar(require('uuid')));
700
+ for (let i = 0; i < 3; i++) {
701
+ const daysAgo = Math.random() * 7;
702
+ const start = new Date(Date.now() - daysAgo * 86400000);
703
+ const end = new Date(start.getTime() + (10 + Math.random() * 30) * 60000);
704
+ clawck.upsert({
705
+ id: uuid(),
706
+ task: pick(tasks),
707
+ project: pick(projects),
708
+ client: pick(clients),
709
+ category: pick(categories),
710
+ agent: pick(agents),
711
+ model: pick(models),
712
+ start: start.toISOString(),
713
+ end: end.toISOString(),
714
+ status: 'failed',
715
+ tokens_in: Math.round(500 + Math.random() * 5000),
716
+ tokens_out: Math.round(100 + Math.random() * 1000),
717
+ cost_usd: Math.round(Math.random() * 0.05 * 10000) / 10000,
718
+ tool_calls: Math.round(Math.random() * 5),
719
+ summary: 'Task failed - auto-generated seed entry',
720
+ tags: ['seed', 'failed'],
721
+ source: 'clawck',
722
+ spec_version: '0.1.0',
723
+ });
423
724
  }
424
- console.log(`\n ⏱️🦀 Seeded ${count} entries into Clawck!`);
425
- console.log(` └─ Run: clawck serve\n`);
725
+ console.log(`\n ⏱️🦀 Seeded ${count + 6} entries into Clawck!`);
726
+ console.log(` ├─ ${count} completed entries (~60% approved)`);
727
+ console.log(` ├─ 3 running entries`);
728
+ console.log(` ├─ 3 failed entries`);
729
+ console.log(` │`);
730
+ console.log(` │ Try these:`);
731
+ console.log(` │ clawck list -d ${resolveDataDir(opts)}`);
732
+ console.log(` │ clawck report --format html -d ${resolveDataDir(opts)}`);
733
+ console.log(` │ clawck pattern list -d ${resolveDataDir(opts)}`);
734
+ console.log(` │ clawck entries --approved -d ${resolveDataDir(opts)}`);
735
+ console.log(` └─ clawck serve -d ${resolveDataDir(opts)}\n`);
426
736
  clawck.close();
427
737
  });
428
738
  // ─── List ───────────────────────────────────────────────
429
739
  program
430
740
  .command('list')
431
741
  .description('List time entries in a human-readable table')
432
- .option('-d, --dir <path>', 'Data directory', '.clawck')
742
+ .option('-d, --dir <path>', 'Data directory')
433
743
  .option('--days <number>', 'Number of days to include', '7')
434
744
  .option('--client <name>', 'Filter by client')
435
745
  .option('--project <name>', 'Filter by project')
436
746
  .option('--agent <name>', 'Filter by agent')
437
747
  .option('--limit <n>', 'Max entries', '50')
748
+ .option('--approved', 'Show only approved entries')
749
+ .option('--unapproved', 'Show only unapproved entries')
438
750
  .action(async (opts) => {
439
- const config = loadConfig(opts.dir);
751
+ const config = loadConfig(resolveDataDir(opts));
440
752
  const clawck = await new clawck_1.Clawck(config).ready();
441
753
  const days = parseInt(opts.days) || 7;
442
754
  const from = new Date(Date.now() - days * 86400000).toISOString();
443
755
  const to = new Date().toISOString();
756
+ const approvedFilter = opts.approved ? true : opts.unapproved ? false : undefined;
444
757
  const entries = clawck.query({
445
758
  client: opts.client,
446
759
  project: opts.project,
@@ -448,6 +761,7 @@ program
448
761
  from,
449
762
  to,
450
763
  limit: parseInt(opts.limit) || 50,
764
+ approved: approvedFilter,
451
765
  });
452
766
  if (program.opts().json) {
453
767
  console.log(JSON.stringify(entries));
@@ -461,9 +775,9 @@ program
461
775
  program
462
776
  .command('delete <id>')
463
777
  .description('Delete a time entry by ID (supports 8-char prefix)')
464
- .option('-d, --dir <path>', 'Data directory', '.clawck')
778
+ .option('-d, --dir <path>', 'Data directory')
465
779
  .action(async (id, opts) => {
466
- const config = loadConfig(opts.dir);
780
+ const config = loadConfig(resolveDataDir(opts));
467
781
  const clawck = await new clawck_1.Clawck(config).ready();
468
782
  const entry = resolveEntryId(clawck, id);
469
783
  if (!entry) {
@@ -484,7 +798,7 @@ program
484
798
  program
485
799
  .command('edit <id>')
486
800
  .description('Edit a time entry by ID (supports 8-char prefix)')
487
- .option('-d, --dir <path>', 'Data directory', '.clawck')
801
+ .option('-d, --dir <path>', 'Data directory')
488
802
  .option('--task <text>', 'Update task description')
489
803
  .option('--duration <minutes>', 'Update duration (recalculates end)', parseFloat)
490
804
  .option('--project <name>', 'Update project')
@@ -493,7 +807,7 @@ program
493
807
  .option('--category <type>', 'Update category')
494
808
  .option('--summary <text>', 'Update summary')
495
809
  .action(async (id, opts) => {
496
- const config = loadConfig(opts.dir);
810
+ const config = loadConfig(resolveDataDir(opts));
497
811
  const clawck = await new clawck_1.Clawck(config).ready();
498
812
  const entry = resolveEntryId(clawck, id);
499
813
  if (!entry) {
@@ -533,14 +847,14 @@ program
533
847
  program
534
848
  .command('export')
535
849
  .description('Export time entries as JSON or CSV')
536
- .option('-d, --dir <path>', 'Data directory', '.clawck')
850
+ .option('-d, --dir <path>', 'Data directory')
537
851
  .option('--format <type>', 'Output format (json or csv)', 'json')
538
852
  .option('--days <number>', 'Number of days to include', '7')
539
853
  .option('--client <name>', 'Filter by client')
540
854
  .option('--project <name>', 'Filter by project')
541
855
  .option('--agent <name>', 'Filter by agent')
542
856
  .action(async (opts) => {
543
- const config = loadConfig(opts.dir);
857
+ const config = loadConfig(resolveDataDir(opts));
544
858
  const clawck = await new clawck_1.Clawck(config).ready();
545
859
  const days = parseInt(opts.days) || 7;
546
860
  const from = new Date(Date.now() - days * 86400000).toISOString();
@@ -560,7 +874,7 @@ program
560
874
  }
561
875
  return val;
562
876
  };
563
- console.log('id,date,task,category,duration_minutes,project,client,agent,model,status,tokens_in,tokens_out,cost_usd,summary');
877
+ console.log('id,date,task,category,duration_minutes,project,client,agent,model,status,tokens_in,tokens_out,cost_usd,summary,created_at,updated_at');
564
878
  for (const e of entries) {
565
879
  const durationMin = e.end
566
880
  ? ((new Date(e.end).getTime() - new Date(e.start).getTime()) / 60000).toFixed(2)
@@ -581,6 +895,8 @@ program
581
895
  String(e.tokens_out),
582
896
  e.cost_usd.toFixed(4),
583
897
  csvEscape(e.summary),
898
+ csvEscape(e.created_at || ''),
899
+ csvEscape(e.updated_at || ''),
584
900
  ].join(','));
585
901
  }
586
902
  }
@@ -589,10 +905,114 @@ program
589
905
  }
590
906
  clawck.close();
591
907
  });
908
+ // ─── Hook (runtime, singular) ────────────────────────────
909
+ const hook = program
910
+ .command('hook')
911
+ .description('Runtime hook command (called by platform hooks, reads JSON from stdin)');
912
+ hook
913
+ .command('start')
914
+ .description('Start tracking — called by platform hooks')
915
+ .option('-d, --dir <path>', 'Data directory')
916
+ .option('--platform <name>', 'Force platform (claude|gemini|cursor|cline|windsurf|codex)')
917
+ .action(async (opts) => {
918
+ try {
919
+ const raw = await (0, hooks_1.readStdin)();
920
+ let json = {};
921
+ if (raw.trim()) {
922
+ try {
923
+ json = JSON.parse(raw);
924
+ }
925
+ catch {
926
+ json = {};
927
+ }
928
+ }
929
+ const platform = opts.platform || undefined;
930
+ const context = (0, hooks_1.normalize)(json, platform);
931
+ const config = loadConfig(resolveDataDir(opts));
932
+ await (0, hooks_1.handleHookStart)(config, context);
933
+ }
934
+ catch (err) {
935
+ const msg = err instanceof Error ? err.message : String(err);
936
+ process.stderr.write(`clawck: hook start failed: ${msg}\n`);
937
+ }
938
+ process.exit(0);
939
+ });
940
+ hook
941
+ .command('stop')
942
+ .description('Stop tracking — called by platform hooks')
943
+ .option('-d, --dir <path>', 'Data directory')
944
+ .option('--platform <name>', 'Force platform (claude|gemini|cursor|cline|windsurf|codex)')
945
+ .action(async (opts) => {
946
+ try {
947
+ const raw = await (0, hooks_1.readStdin)();
948
+ let json = {};
949
+ if (raw.trim()) {
950
+ try {
951
+ json = JSON.parse(raw);
952
+ }
953
+ catch {
954
+ json = {};
955
+ }
956
+ }
957
+ const platform = opts.platform || undefined;
958
+ const context = (0, hooks_1.normalize)(json, platform);
959
+ const config = loadConfig(resolveDataDir(opts));
960
+ await (0, hooks_1.handleHookStop)(config, context);
961
+ }
962
+ catch (err) {
963
+ const msg = err instanceof Error ? err.message : String(err);
964
+ process.stderr.write(`clawck: hook stop failed: ${msg}\n`);
965
+ }
966
+ process.exit(0);
967
+ });
968
+ // ─── Hooks (management, plural) ──────────────────────────
969
+ const hooks = program
970
+ .command('hooks')
971
+ .description('Manage platform hook integrations');
972
+ hooks
973
+ .command('install <platform>')
974
+ .description('Output hook configuration for a platform')
975
+ .action(async (platformName) => {
976
+ const key = platformName.toLowerCase();
977
+ if (!hooks_1.PLATFORMS[key]) {
978
+ console.error(` Unknown platform: "${platformName}"`);
979
+ console.error(` Available: ${hooks_1.PLATFORM_NAMES.join(', ')}`);
980
+ process.exit(1);
981
+ }
982
+ const info = hooks_1.PLATFORMS[key];
983
+ console.log(`\n ⏱️🦀 Clawck Hooks — ${info.displayName}`);
984
+ console.log(` ${'─'.repeat(40)}`);
985
+ if (info.configPaths.length > 0) {
986
+ console.log(`\n Add to: ${info.configPaths[0]}\n`);
987
+ }
988
+ console.log(info.generate());
989
+ console.log(`\n Or copy from: docs/snippets/${info.snippetFile}`);
990
+ console.log('');
991
+ });
992
+ hooks
993
+ .command('status')
994
+ .description('Show which platforms have clawck hooks installed')
995
+ .action(async () => {
996
+ console.log(`\n ⏱️🦀 Clawck Hooks — Status`);
997
+ console.log(` ${'─'.repeat(40)}`);
998
+ for (const name of hooks_1.PLATFORM_NAMES) {
999
+ const info = hooks_1.PLATFORMS[name];
1000
+ const installed = info.detect();
1001
+ const icon = installed ? '✓' : '✗';
1002
+ console.log(` ${icon} ${info.displayName.padEnd(16)} ${installed ? 'installed' : 'not found'}`);
1003
+ }
1004
+ console.log(`\n Run "clawck hooks install <platform>" to set up a platform.\n`);
1005
+ });
592
1006
  // ─── Helpers ──────────────────────────────────────────────
1007
+ function resolveDataDir(subcommandOpts) {
1008
+ return subcommandOpts.dir
1009
+ ?? program.opts().dir
1010
+ ?? process.env.CLAWCK_DIR
1011
+ ?? '.clawck';
1012
+ }
593
1013
  function formatDuration(mins) {
594
1014
  if (mins < 1)
595
- return '<1 min';
1015
+ return `${Math.round(mins * 60)}s`;
596
1016
  if (mins < 60)
597
1017
  return `${Math.round(mins)} min`;
598
1018
  const h = Math.floor(mins / 60);
@@ -632,7 +1052,7 @@ function printEntryTable(entries) {
632
1052
  console.log('\n No entries found.\n');
633
1053
  return;
634
1054
  }
635
- const header = ` ${'ID'.padEnd(10)} ${'Task'.padEnd(42)} ${'Duration'.padEnd(10)} ${'Project'.padEnd(12)} ${'Agent'.padEnd(12)} ${'Time'}`;
1055
+ const header = ` ${'ID'.padEnd(10)} ${'Task'.padEnd(42)} ${'Duration'.padEnd(10)} ${'Project'.padEnd(12)} ${'Agent'.padEnd(12)} ${'OK'.padEnd(4)} ${'Time'}`;
636
1056
  console.log(`\n${header}`);
637
1057
  console.log(` ${'─'.repeat(header.length - 2)}`);
638
1058
  for (const e of entries) {
@@ -642,7 +1062,8 @@ function printEntryTable(entries) {
642
1062
  const dur = e.end ? formatDuration(durationMin) : 'running';
643
1063
  const task = e.task.length > 40 ? e.task.slice(0, 37) + '...' : e.task;
644
1064
  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}`);
1065
+ const approved = e.approved ? 'v' : '-';
1066
+ 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)} ${approved.padEnd(4)} ${time}`);
646
1067
  }
647
1068
  console.log(`\n ${entries.length} entries\n`);
648
1069
  }
@@ -682,6 +1103,22 @@ function loadConfig(dir) {
682
1103
  console.error(' Config error: "remote_sources" must be an array');
683
1104
  process.exit(1);
684
1105
  }
1106
+ if (fileConfig.webhooks !== undefined) {
1107
+ if (!Array.isArray(fileConfig.webhooks)) {
1108
+ console.error(' Config error: "webhooks" must be an array');
1109
+ process.exit(1);
1110
+ }
1111
+ for (const [i, wh] of fileConfig.webhooks.entries()) {
1112
+ if (!wh.url || typeof wh.url !== 'string') {
1113
+ console.error(` Config error: "webhooks[${i}].url" must be a string`);
1114
+ process.exit(1);
1115
+ }
1116
+ if (!Array.isArray(wh.events)) {
1117
+ console.error(` Config error: "webhooks[${i}].events" must be an array`);
1118
+ process.exit(1);
1119
+ }
1120
+ }
1121
+ }
685
1122
  return {
686
1123
  ...types_1.DEFAULT_CONFIG,
687
1124
  ...fileConfig,