clawck 0.1.3 → 0.4.1

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 (106) hide show
  1. package/README.md +251 -174
  2. package/dist/cli/index.js +947 -87
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/core/atp.d.ts +31 -0
  5. package/dist/core/atp.d.ts.map +1 -0
  6. package/dist/core/atp.js +96 -0
  7. package/dist/core/atp.js.map +1 -0
  8. package/dist/core/benchmarks.d.ts +20 -0
  9. package/dist/core/benchmarks.d.ts.map +1 -0
  10. package/dist/core/benchmarks.js +86 -0
  11. package/dist/core/benchmarks.js.map +1 -0
  12. package/dist/core/clawck.d.ts +39 -3
  13. package/dist/core/clawck.d.ts.map +1 -1
  14. package/dist/core/clawck.js +140 -18
  15. package/dist/core/clawck.js.map +1 -1
  16. package/dist/core/database.d.ts +16 -1
  17. package/dist/core/database.d.ts.map +1 -1
  18. package/dist/core/database.js +177 -8
  19. package/dist/core/database.js.map +1 -1
  20. package/dist/core/patterns.d.ts +7 -0
  21. package/dist/core/patterns.d.ts.map +1 -0
  22. package/dist/core/patterns.js +36 -0
  23. package/dist/core/patterns.js.map +1 -0
  24. package/dist/core/personal.d.ts +26 -0
  25. package/dist/core/personal.d.ts.map +1 -0
  26. package/dist/core/personal.js +60 -0
  27. package/dist/core/personal.js.map +1 -0
  28. package/dist/core/runtime.d.ts +17 -0
  29. package/dist/core/runtime.d.ts.map +1 -0
  30. package/dist/core/runtime.js +43 -0
  31. package/dist/core/runtime.js.map +1 -0
  32. package/dist/core/types.d.ts +93 -2
  33. package/dist/core/types.d.ts.map +1 -1
  34. package/dist/core/types.js +9 -2
  35. package/dist/core/types.js.map +1 -1
  36. package/dist/core/webhooks.d.ts +22 -0
  37. package/dist/core/webhooks.d.ts.map +1 -0
  38. package/dist/core/webhooks.js +70 -0
  39. package/dist/core/webhooks.js.map +1 -0
  40. package/dist/hooks/adapters.d.ts +8 -0
  41. package/dist/hooks/adapters.d.ts.map +1 -0
  42. package/dist/hooks/adapters.js +152 -0
  43. package/dist/hooks/adapters.js.map +1 -0
  44. package/dist/hooks/handler.d.ts +9 -0
  45. package/dist/hooks/handler.d.ts.map +1 -0
  46. package/dist/hooks/handler.js +93 -0
  47. package/dist/hooks/handler.js.map +1 -0
  48. package/dist/hooks/index.d.ts +10 -0
  49. package/dist/hooks/index.d.ts.map +1 -0
  50. package/dist/hooks/index.js +23 -0
  51. package/dist/hooks/index.js.map +1 -0
  52. package/dist/hooks/install.d.ts +16 -0
  53. package/dist/hooks/install.d.ts.map +1 -0
  54. package/dist/hooks/install.js +247 -0
  55. package/dist/hooks/install.js.map +1 -0
  56. package/dist/hooks/session.d.ts +10 -0
  57. package/dist/hooks/session.d.ts.map +1 -0
  58. package/dist/hooks/session.js +72 -0
  59. package/dist/hooks/session.js.map +1 -0
  60. package/dist/hooks/stdin.d.ts +6 -0
  61. package/dist/hooks/stdin.d.ts.map +1 -0
  62. package/dist/hooks/stdin.js +38 -0
  63. package/dist/hooks/stdin.js.map +1 -0
  64. package/dist/hooks/types.d.ts +26 -0
  65. package/dist/hooks/types.d.ts.map +1 -0
  66. package/dist/hooks/types.js +7 -0
  67. package/dist/hooks/types.js.map +1 -0
  68. package/dist/index.d.ts +10 -0
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +19 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/reports/html.d.ts +14 -0
  73. package/dist/reports/html.d.ts.map +1 -0
  74. package/dist/reports/html.js +353 -0
  75. package/dist/reports/html.js.map +1 -0
  76. package/dist/reports/pdf.d.ts +14 -0
  77. package/dist/reports/pdf.d.ts.map +1 -0
  78. package/dist/reports/pdf.js +177 -0
  79. package/dist/reports/pdf.js.map +1 -0
  80. package/dist/reports/periods.d.ts +18 -0
  81. package/dist/reports/periods.d.ts.map +1 -0
  82. package/dist/reports/periods.js +55 -0
  83. package/dist/reports/periods.js.map +1 -0
  84. package/dist/server/api.d.ts.map +1 -1
  85. package/dist/server/api.js +156 -2
  86. package/dist/server/api.js.map +1 -1
  87. package/dist/server/mcp.d.ts.map +1 -1
  88. package/dist/server/mcp.js +31 -10
  89. package/dist/server/mcp.js.map +1 -1
  90. package/docs/atp-schema.json +298 -0
  91. package/docs/atp-spec-v0.2.md +216 -0
  92. package/docs/benchmarks-sources.md +202 -0
  93. package/docs/platform-testing-guide.md +423 -0
  94. package/docs/skills/clawck-setup.md +131 -0
  95. package/docs/skills/clawck-usage.md +148 -0
  96. package/docs/snippets/claude-md.txt +33 -0
  97. package/docs/snippets/hooks-claude.json +24 -0
  98. package/docs/snippets/hooks-cline.txt +10 -0
  99. package/docs/snippets/hooks-codex.json +16 -0
  100. package/docs/snippets/hooks-cursor.json +16 -0
  101. package/docs/snippets/hooks-gemini.json +16 -0
  102. package/docs/snippets/hooks-windsurf.json +16 -0
  103. package/docs/snippets/mcp-config.json +7 -0
  104. package/docs/snippets/openclaw-agent-md.txt +18 -0
  105. package/docs/snippets/openclaw-heartbeat-md.txt +17 -0
  106. 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,33 @@ 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 periods_1 = require("../reports/periods");
61
+ const patterns_1 = require("../core/patterns");
62
+ const hooks_1 = require("../hooks");
63
+ const benchmarks_1 = require("../core/benchmarks");
64
+ const atp_1 = require("../core/atp");
65
+ const types_2 = require("../core/types");
25
66
  const program = new commander_1.Command();
26
67
  program
27
68
  .name('clawck')
28
69
  .description('⏱️🦀 Clawck — Time tracking for AI agents')
29
- .version('0.1.3')
30
- .option('--json', 'Output as JSON (for scripting/pipelines)');
70
+ .version('0.4.0')
71
+ .enablePositionalOptions()
72
+ .option('--json', 'Output as JSON (for scripting/pipelines)')
73
+ .option('-d, --dir <path>', 'Data directory (also: CLAWCK_DIR env var)');
31
74
  // ─── Init ─────────────────────────────────────────────────
32
75
  program
33
76
  .command('init')
34
77
  .description('Initialize a .clawck/ directory in the current folder')
35
- .option('-d, --dir <path>', 'Data directory', '.clawck')
78
+ .option('-d, --dir <path>', 'Data directory')
36
79
  .action(async (opts) => {
37
- const dir = path_1.default.resolve(opts.dir);
80
+ const resolvedDir = resolveDataDir(opts);
81
+ const dir = path_1.default.resolve(resolvedDir);
38
82
  // Prevent nesting: don't create .clawck inside an existing .clawck
39
83
  const cwd = process.cwd();
40
- if (path_1.default.basename(cwd) === '.clawck' && opts.dir === '.clawck') {
84
+ if (path_1.default.basename(cwd) === '.clawck' && resolvedDir === '.clawck') {
41
85
  console.error(' Already inside a .clawck directory. Aborting to prevent nesting.');
42
86
  process.exit(1);
43
87
  }
@@ -59,6 +103,8 @@ program
59
103
  default_source: 'clawck',
60
104
  human_equivalents: types_1.DEFAULT_HUMAN_EQUIVALENTS,
61
105
  remote_sources: [],
106
+ patterns: patterns_1.DEFAULT_PATTERNS,
107
+ default_pattern: 'default',
62
108
  };
63
109
  fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
64
110
  }
@@ -78,9 +124,9 @@ program
78
124
  .command('serve')
79
125
  .description('Start the Clawck API server and dashboard')
80
126
  .option('-p, --port <number>', 'Port number', '3456')
81
- .option('-d, --dir <path>', 'Data directory', '.clawck')
127
+ .option('-d, --dir <path>', 'Data directory')
82
128
  .action(async (opts) => {
83
- const config = loadConfig(opts.dir);
129
+ const config = loadConfig(resolveDataDir(opts));
84
130
  config.port = parseInt(opts.port) || config.port;
85
131
  (0, api_1.startServer)(config);
86
132
  });
@@ -88,18 +134,18 @@ program
88
134
  program
89
135
  .command('mcp')
90
136
  .description('Start the Clawck MCP server (stdio, for Claude Code / Cline / Cursor)')
91
- .option('-d, --dir <path>', 'Data directory', '.clawck')
137
+ .option('-d, --dir <path>', 'Data directory')
92
138
  .action(async (opts) => {
93
- const config = loadConfig(opts.dir);
94
- (0, mcp_1.startMCPServer)(config);
139
+ const config = loadConfig(resolveDataDir(opts));
140
+ await (0, mcp_1.startMCPServer)(config);
95
141
  });
96
142
  // ─── Status ───────────────────────────────────────────────
97
143
  program
98
144
  .command('status')
99
145
  .description('Show currently running tasks and stats')
100
- .option('-d, --dir <path>', 'Data directory', '.clawck')
146
+ .option('-d, --dir <path>', 'Data directory')
101
147
  .action(async (opts) => {
102
- const config = loadConfig(opts.dir);
148
+ const config = loadConfig(resolveDataDir(opts));
103
149
  const clawck = await new clawck_1.Clawck(config).ready();
104
150
  const stats = clawck.stats();
105
151
  const running = clawck.running();
@@ -126,73 +172,236 @@ program
126
172
  clawck.close();
127
173
  });
128
174
  // ─── Report ───────────────────────────────────────────────
129
- program
175
+ const reportCmd = program
130
176
  .command('report')
131
177
  .description('Show a timesheet summary')
132
- .option('-d, --dir <path>', 'Data directory', '.clawck')
133
- .option('--days <number>', 'Number of days to include', '7')
178
+ .option('-d, --dir <path>', 'Data directory')
179
+ .option('--days <number>', 'Number of days to include')
180
+ .option('--period <type>', 'Time period (day, week, month, year, custom)')
181
+ .option('--from <date>', 'Start date (YYYY-MM-DD or ISO)')
182
+ .option('--to <date>', 'End date (YYYY-MM-DD or ISO)')
183
+ .option('--style <type>', 'Report style (full, short, visual, text, table, calendar)', 'full')
134
184
  .option('--client <name>', 'Filter by client')
135
185
  .option('--project <name>', 'Filter by project')
136
186
  .option('--agent <name>', 'Filter by agent')
187
+ .option('--format <type>', 'Output format (terminal, pdf, or html)', 'terminal')
188
+ .option('--output <path>', 'Output file path (for pdf/html format)')
137
189
  .option('--detailed', 'Show individual entries')
190
+ .option('--save', 'Save report to database')
191
+ .option('--name <name>', 'Name for saved report')
138
192
  .action(async (opts) => {
139
- const config = loadConfig(opts.dir);
193
+ const config = loadConfig(resolveDataDir(opts));
140
194
  const clawck = await new clawck_1.Clawck(config).ready();
141
- const days = parseInt(opts.days) || 7;
142
- const to = new Date().toISOString();
143
- const from = new Date(Date.now() - days * 86400000).toISOString();
195
+ // Resolve time period
196
+ const resolved = (0, periods_1.resolvePeriod)({
197
+ period: opts.period,
198
+ from: opts.from,
199
+ to: opts.to,
200
+ days: opts.days ? parseInt(opts.days) : undefined,
201
+ });
202
+ const { from, to, period } = resolved;
203
+ // --detailed maps to style='table' when no explicit --style
204
+ const style = opts.detailed && opts.style === 'full' ? 'table' : opts.style;
144
205
  const ts = clawck.timesheet(from, to, {
145
206
  client: opts.client,
146
207
  project: opts.project,
147
208
  agent: opts.agent,
148
209
  });
210
+ const periodLabel = opts.period || (opts.days ? `${opts.days} days` : period);
211
+ let reportContent = '';
212
+ if (opts.format === 'html') {
213
+ const today = new Date().toISOString().split('T')[0];
214
+ const outputPath = opts.output || `clawck-report-${today}.html`;
215
+ const dateRange = `${from.split('T')[0]} to ${to.split('T')[0]}`;
216
+ const rawEntries = clawck.query({ from, to, client: opts.client, project: opts.project, agent: opts.agent, limit: 10000 });
217
+ const html = (0, html_1.generateTimesheetHTML)(ts, { dateRange, clientName: opts.client, rawEntries, style });
218
+ reportContent = html;
219
+ fs_1.default.writeFileSync(outputPath, html);
220
+ console.log(` HTML report saved to: ${outputPath}`);
221
+ }
222
+ else if (opts.format === 'pdf') {
223
+ const today = new Date().toISOString().split('T')[0];
224
+ const outputPath = opts.output || `clawck-report-${today}.pdf`;
225
+ const dateRange = `${from.split('T')[0]} to ${to.split('T')[0]}`;
226
+ await (0, pdf_1.generateTimesheetPDF)(ts, {
227
+ clientName: opts.client,
228
+ dateRange,
229
+ outputPath,
230
+ style,
231
+ });
232
+ reportContent = fs_1.default.readFileSync(outputPath).toString('base64');
233
+ console.log(` PDF report saved to: ${outputPath}`);
234
+ }
235
+ else {
236
+ // Terminal output
237
+ if (program.opts().json) {
238
+ reportContent = JSON.stringify(ts);
239
+ console.log(reportContent);
240
+ }
241
+ else {
242
+ const totalAgentRuntimeMin = ts.entries.reduce((s, r) => s + (r.agent_runtime_minutes || 0), 0);
243
+ if (style === 'table') {
244
+ const entries = clawck.query({ client: opts.client, project: opts.project, agent: opts.agent, from, to, limit: 500 });
245
+ printEntryTable(entries);
246
+ }
247
+ else if (style === 'short') {
248
+ console.log(`\n 📋 Clawck Timesheet — ${periodLabel}`);
249
+ console.log(` ${'─'.repeat(50)}`);
250
+ console.log(` ⏱️ Wall-clock hours: ${ts.total_agent_hours.toFixed(2)} hrs`);
251
+ if (totalAgentRuntimeMin > 0) {
252
+ console.log(` 🤖 Agent runtime: ${formatDuration(totalAgentRuntimeMin)} (estimated)`);
253
+ }
254
+ console.log(` 👤 Human equiv: ${ts.total_human_equiv_hours.toFixed(2)} hrs`);
255
+ console.log(` 💰 Agent cost: $${ts.total_cost_usd.toFixed(2)}`);
256
+ console.log(` 💚 Est. savings: $${ts.total_savings_usd.toFixed(0)}`);
257
+ console.log(` 🔢 Total entries: ${ts.total_entries}`);
258
+ console.log(` 🪙 Total tokens: ${ts.total_tokens.toLocaleString()}`);
259
+ console.log('');
260
+ }
261
+ else {
262
+ // full, text, visual, calendar all fall back to full terminal output
263
+ console.log(`\n 📋 Clawck Timesheet — ${periodLabel}`);
264
+ console.log(` ${'─'.repeat(50)}`);
265
+ console.log(` ⏱️ Wall-clock hours: ${ts.total_agent_hours.toFixed(2)} hrs`);
266
+ if (totalAgentRuntimeMin > 0) {
267
+ console.log(` 🤖 Agent runtime: ${formatDuration(totalAgentRuntimeMin)} (estimated)`);
268
+ }
269
+ console.log(` 👤 Human equiv: ${ts.total_human_equiv_hours.toFixed(2)} hrs`);
270
+ console.log(` 💰 Agent cost: $${ts.total_cost_usd.toFixed(2)}`);
271
+ console.log(` 💚 Est. savings: $${ts.total_savings_usd.toFixed(0)}`);
272
+ console.log(` 🔢 Total entries: ${ts.total_entries}`);
273
+ console.log(` 🪙 Total tokens: ${ts.total_tokens.toLocaleString()}`);
274
+ if (ts.by_project.length > 0) {
275
+ console.log(`\n 📁 By Project:`);
276
+ for (const p of ts.by_project) {
277
+ const bar = '█'.repeat(Math.max(1, Math.round(p.agent_hours / (ts.total_agent_hours || 1) * 20)));
278
+ console.log(` ${bar} ${p.project} (${p.client}): ${p.agent_hours.toFixed(2)}h → ${p.human_equiv_hours.toFixed(2)}h human equiv`);
279
+ }
280
+ }
281
+ if (ts.by_agent.length > 0) {
282
+ console.log(`\n 🤖 By Agent:`);
283
+ for (const a of ts.by_agent) {
284
+ console.log(` • ${a.agent} (${a.model}): ${a.agent_hours.toFixed(2)}h, ${a.success_rate}% success`);
285
+ }
286
+ }
287
+ console.log('');
288
+ }
289
+ }
290
+ }
291
+ // Save report if requested
292
+ if (opts.save) {
293
+ const metadata = {
294
+ filters: { client: opts.client, project: opts.project, agent: opts.agent },
295
+ total_entries: ts.total_entries,
296
+ total_agent_hours: ts.total_agent_hours,
297
+ total_cost_usd: ts.total_cost_usd,
298
+ total_savings_usd: ts.total_savings_usd,
299
+ };
300
+ const saved = clawck.saveReport({
301
+ name: opts.name || `Report ${new Date().toISOString().split('T')[0]}`,
302
+ period,
303
+ period_start: from,
304
+ period_end: to,
305
+ style,
306
+ format: opts.format,
307
+ content: reportContent || JSON.stringify(ts),
308
+ metadata,
309
+ });
310
+ console.log(` Report saved: ${saved.id.slice(0, 8)}`);
311
+ }
312
+ clawck.close();
313
+ });
314
+ // ─── Report Subcommands ─────────────────────────────────
315
+ reportCmd
316
+ .command('list')
317
+ .description('List saved reports')
318
+ .option('-d, --dir <path>', 'Data directory')
319
+ .option('--limit <n>', 'Max reports to show', '20')
320
+ .action(async (opts) => {
321
+ const config = loadConfig(resolveDataDir(opts));
322
+ const clawck = await new clawck_1.Clawck(config).ready();
323
+ const reports = clawck.listReports(parseInt(opts.limit) || 20);
149
324
  if (program.opts().json) {
150
- console.log(JSON.stringify(ts));
325
+ console.log(JSON.stringify(reports));
151
326
  clawck.close();
152
327
  return;
153
328
  }
154
- console.log(`\n 📋 Clawck Timesheet — Last ${days} days`);
155
- console.log(` ${'─'.repeat(50)}`);
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`);
158
- console.log(` 💰 Agent cost: $${ts.total_cost_usd.toFixed(2)}`);
159
- console.log(` 💚 Est. savings: $${ts.total_savings_usd.toFixed(0)}`);
160
- console.log(` 🔢 Total entries: ${ts.total_entries}`);
161
- console.log(` 🪙 Total tokens: ${ts.total_tokens.toLocaleString()}`);
162
- if (ts.by_project.length > 0) {
163
- console.log(`\n 📁 By Project:`);
164
- for (const p of ts.by_project) {
165
- const bar = '█'.repeat(Math.max(1, Math.round(p.agent_hours / (ts.total_agent_hours || 1) * 20)));
166
- console.log(` ${bar} ${p.project} (${p.client}): ${p.agent_hours.toFixed(2)}h → ${p.human_equiv_hours.toFixed(2)}h human equiv`);
167
- }
168
- }
169
- if (ts.by_agent.length > 0) {
170
- console.log(`\n 🤖 By Agent:`);
171
- for (const a of ts.by_agent) {
172
- console.log(` • ${a.agent} (${a.model}): ${a.agent_hours.toFixed(2)}h, ${a.success_rate}% success`);
173
- }
329
+ if (reports.length === 0) {
330
+ console.log('\n No saved reports.\n');
331
+ clawck.close();
332
+ return;
174
333
  }
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);
334
+ console.log(`\n Saved Reports:`);
335
+ console.log(` ${'ID'.padEnd(10)} ${'Name'.padEnd(30)} ${'Period'.padEnd(8)} ${'Style'.padEnd(10)} ${'Format'.padEnd(10)} ${'Date'}`);
336
+ console.log(` ${'─'.repeat(80)}`);
337
+ for (const r of reports) {
338
+ console.log(` ${r.id.slice(0, 8).padEnd(10)} ${(r.name || '-').slice(0, 28).padEnd(30)} ${r.period.padEnd(8)} ${r.style.padEnd(10)} ${r.format.padEnd(10)} ${r.created_at.split('T')[0]}`);
179
339
  }
180
340
  console.log('');
181
341
  clawck.close();
182
342
  });
343
+ reportCmd
344
+ .command('show <id>')
345
+ .description('Retrieve a saved report')
346
+ .option('-d, --dir <path>', 'Data directory')
347
+ .option('--output <path>', 'Write content to file')
348
+ .action(async (id, opts) => {
349
+ const config = loadConfig(resolveDataDir(opts));
350
+ const clawck = await new clawck_1.Clawck(config).ready();
351
+ const report = clawck.getReport(id);
352
+ if (!report) {
353
+ console.error(` Report not found: ${id}`);
354
+ clawck.close();
355
+ process.exit(1);
356
+ }
357
+ if (opts.output) {
358
+ const content = typeof report.content === 'string' ? report.content : report.content;
359
+ fs_1.default.writeFileSync(opts.output, content);
360
+ console.log(` Report written to: ${opts.output}`);
361
+ }
362
+ else if (program.opts().json) {
363
+ console.log(JSON.stringify(report));
364
+ }
365
+ else {
366
+ console.log(typeof report.content === 'string' ? report.content : '[binary content — use --output to save]');
367
+ }
368
+ clawck.close();
369
+ });
370
+ reportCmd
371
+ .command('delete <id>')
372
+ .description('Delete a saved report')
373
+ .option('-d, --dir <path>', 'Data directory')
374
+ .action(async (id, opts) => {
375
+ const config = loadConfig(resolveDataDir(opts));
376
+ const clawck = await new clawck_1.Clawck(config).ready();
377
+ const deleted = clawck.deleteReport(id);
378
+ if (!deleted) {
379
+ console.error(` Report not found: ${id}`);
380
+ clawck.close();
381
+ process.exit(1);
382
+ }
383
+ if (program.opts().json) {
384
+ console.log(JSON.stringify({ ok: true }));
385
+ }
386
+ else {
387
+ console.log(` Report deleted.`);
388
+ }
389
+ clawck.close();
390
+ });
183
391
  // ─── Start ───────────────────────────────────────────────
184
392
  program
185
393
  .command('start <task>')
186
394
  .description('Start tracking time for a task')
187
- .option('-d, --dir <path>', 'Data directory', '.clawck')
395
+ .option('-d, --dir <path>', 'Data directory')
188
396
  .option('--project <name>', 'Project name')
189
397
  .option('--client <name>', 'Client name')
190
398
  .option('--category <type>', 'Task category')
191
399
  .option('--agent <name>', 'Agent name')
192
400
  .option('--model <name>', 'Model name')
193
401
  .option('--tags <tags...>', 'Tags')
402
+ .option('--pattern <name>', 'Use a tracking pattern')
194
403
  .action(async (task, opts) => {
195
- const config = loadConfig(opts.dir);
404
+ const config = loadConfig(resolveDataDir(opts));
196
405
  const clawck = await new clawck_1.Clawck(config).ready();
197
406
  const entry = clawck.start({
198
407
  task,
@@ -202,6 +411,7 @@ program
202
411
  agent: opts.agent,
203
412
  model: opts.model,
204
413
  tags: opts.tags,
414
+ pattern: opts.pattern,
205
415
  });
206
416
  if (program.opts().json) {
207
417
  console.log(JSON.stringify(entry));
@@ -217,7 +427,7 @@ program
217
427
  program
218
428
  .command('stop <id>')
219
429
  .description('Stop tracking time for a task')
220
- .option('-d, --dir <path>', 'Data directory', '.clawck')
430
+ .option('-d, --dir <path>', 'Data directory')
221
431
  .option('--status <status>', 'Outcome (completed/failed)', 'completed')
222
432
  .option('--summary <text>', 'Summary of work done')
223
433
  .option('--tokens-in <n>', 'Input tokens consumed', parseFloat)
@@ -225,7 +435,7 @@ program
225
435
  .option('--cost <n>', 'Cost in USD', parseFloat)
226
436
  .option('--tool-calls <n>', 'Number of tool calls', parseInt)
227
437
  .action(async (id, opts) => {
228
- const config = loadConfig(opts.dir);
438
+ const config = loadConfig(resolveDataDir(opts));
229
439
  const clawck = await new clawck_1.Clawck(config).ready();
230
440
  const entry = clawck.stop({
231
441
  id,
@@ -247,14 +457,17 @@ program
247
457
  process.exit(1);
248
458
  }
249
459
  const duration_minutes = entry.end
250
- ? +((new Date(entry.end).getTime() - new Date(entry.start).getTime()) / 60000).toFixed(1)
460
+ ? (new Date(entry.end).getTime() - new Date(entry.start).getTime()) / 60000
251
461
  : null;
252
462
  if (program.opts().json) {
253
- console.log(JSON.stringify({ ...entry, duration_minutes }));
463
+ console.log(JSON.stringify({ ...entry, duration_minutes: duration_minutes ? Math.round(duration_minutes * 100) / 100 : null }));
254
464
  }
255
465
  else {
256
466
  console.log(` Stopped: ${entry.task}`);
257
- console.log(` Duration: ${duration_minutes}m Status: ${entry.status}`);
467
+ console.log(` Wall clock: ${duration_minutes !== null ? formatDuration(duration_minutes) : 'unknown'} Status: ${entry.status}`);
468
+ if (entry.agent_runtime_ms != null) {
469
+ console.log(` Agent runtime (est.): ${formatDuration(entry.agent_runtime_ms / 60000)}`);
470
+ }
258
471
  }
259
472
  clawck.close();
260
473
  });
@@ -262,7 +475,7 @@ program
262
475
  program
263
476
  .command('log <task>')
264
477
  .description('Log a completed task retroactively')
265
- .option('-d, --dir <path>', 'Data directory', '.clawck')
478
+ .option('-d, --dir <path>', 'Data directory')
266
479
  .option('--duration <minutes>', 'Duration in minutes', parseFloat)
267
480
  .option('--project <name>', 'Project name')
268
481
  .option('--client <name>', 'Client name')
@@ -271,12 +484,13 @@ program
271
484
  .option('--model <name>', 'Model name')
272
485
  .option('--summary <text>', 'Summary of work done')
273
486
  .option('--tags <tags...>', 'Tags')
487
+ .option('--pattern <name>', 'Use a tracking pattern')
274
488
  .action(async (task, opts) => {
275
489
  if (!opts.duration) {
276
490
  console.error(' --duration <minutes> is required');
277
491
  process.exit(1);
278
492
  }
279
- const config = loadConfig(opts.dir);
493
+ const config = loadConfig(resolveDataDir(opts));
280
494
  const clawck = await new clawck_1.Clawck(config).ready();
281
495
  const entry = clawck.log({
282
496
  task,
@@ -288,6 +502,7 @@ program
288
502
  model: opts.model,
289
503
  summary: opts.summary,
290
504
  tags: opts.tags,
505
+ pattern: opts.pattern,
291
506
  });
292
507
  if (program.opts().json) {
293
508
  console.log(JSON.stringify(entry));
@@ -302,9 +517,9 @@ program
302
517
  program
303
518
  .command('get <id>')
304
519
  .description('Get a single time entry by ID')
305
- .option('-d, --dir <path>', 'Data directory', '.clawck')
520
+ .option('-d, --dir <path>', 'Data directory')
306
521
  .action(async (id, opts) => {
307
- const config = loadConfig(opts.dir);
522
+ const config = loadConfig(resolveDataDir(opts));
308
523
  const clawck = await new clawck_1.Clawck(config).ready();
309
524
  const entry = clawck.get(id);
310
525
  if (!entry) {
@@ -326,6 +541,8 @@ program
326
541
  console.log(` Project: ${entry.project} Client: ${entry.client}`);
327
542
  console.log(` Agent: ${entry.agent} Model: ${entry.model}`);
328
543
  console.log(` Start: ${entry.start} End: ${entry.end || '(running)'}`);
544
+ console.log(` Approved: ${entry.approved ? 'yes' : 'no'}`);
545
+ console.log(` Created: ${entry.created_at} Updated: ${entry.updated_at}`);
329
546
  }
330
547
  clawck.close();
331
548
  });
@@ -333,7 +550,7 @@ program
333
550
  program
334
551
  .command('entries')
335
552
  .description('Query time entries')
336
- .option('-d, --dir <path>', 'Data directory', '.clawck')
553
+ .option('-d, --dir <path>', 'Data directory')
337
554
  .option('--client <name>', 'Filter by client')
338
555
  .option('--project <name>', 'Filter by project')
339
556
  .option('--agent <name>', 'Filter by agent')
@@ -341,9 +558,12 @@ program
341
558
  .option('--from <date>', 'Start date (ISO 8601)')
342
559
  .option('--to <date>', 'End date (ISO 8601)')
343
560
  .option('--limit <n>', 'Max entries', '50')
561
+ .option('--approved', 'Show only approved entries')
562
+ .option('--unapproved', 'Show only unapproved entries')
344
563
  .action(async (opts) => {
345
- const config = loadConfig(opts.dir);
564
+ const config = loadConfig(resolveDataDir(opts));
346
565
  const clawck = await new clawck_1.Clawck(config).ready();
566
+ const approvedFilter = opts.approved ? true : opts.unapproved ? false : undefined;
347
567
  const entries = clawck.query({
348
568
  client: opts.client,
349
569
  project: opts.project,
@@ -352,6 +572,7 @@ program
352
572
  from: opts.from,
353
573
  to: opts.to,
354
574
  limit: parseInt(opts.limit) || 50,
575
+ approved: approvedFilter,
355
576
  });
356
577
  if (program.opts().json) {
357
578
  console.log(JSON.stringify(entries));
@@ -368,21 +589,195 @@ program
368
589
  console.log('');
369
590
  clawck.close();
370
591
  });
592
+ // ─── Approve ─────────────────────────────────────────────
593
+ program
594
+ .command('approve <id>')
595
+ .description('Approve a time entry (supports 8-char prefix)')
596
+ .option('-d, --dir <path>', 'Data directory')
597
+ .action(async (id, opts) => {
598
+ const config = loadConfig(resolveDataDir(opts));
599
+ const clawck = await new clawck_1.Clawck(config).ready();
600
+ const entry = resolveEntryId(clawck, id);
601
+ if (!entry) {
602
+ clawck.close();
603
+ process.exit(1);
604
+ }
605
+ const approved = clawck.approve(entry.id);
606
+ if (program.opts().json) {
607
+ console.log(JSON.stringify(approved));
608
+ }
609
+ else {
610
+ console.log(` Approved: ${approved.task}`);
611
+ console.log(` ID: ${approved.id.slice(0, 8)} Project: ${approved.project}`);
612
+ }
613
+ clawck.close();
614
+ });
615
+ // ─── Pattern ─────────────────────────────────────────────
616
+ const pattern = program
617
+ .command('pattern')
618
+ .description('Manage tracking patterns (task templates)');
619
+ pattern
620
+ .command('list')
621
+ .description('List all tracking patterns')
622
+ .option('-d, --dir <path>', 'Data directory')
623
+ .action(async (opts) => {
624
+ const config = loadConfig(resolveDataDir(opts));
625
+ const clawck = await new clawck_1.Clawck(config).ready();
626
+ const patterns = clawck.getPatterns();
627
+ if (program.opts().json) {
628
+ console.log(JSON.stringify(patterns));
629
+ clawck.close();
630
+ return;
631
+ }
632
+ console.log(`\n Tracking Patterns:`);
633
+ for (const p of patterns) {
634
+ const defaultMark = config.default_pattern === p.name ? ' (default)' : '';
635
+ console.log(` - ${p.name}${defaultMark}`);
636
+ if (p.description)
637
+ console.log(` ${p.description}`);
638
+ const fields = [];
639
+ if (p.category)
640
+ fields.push(`category: ${p.category}`);
641
+ if (p.project)
642
+ fields.push(`project: ${p.project}`);
643
+ if (p.client)
644
+ fields.push(`client: ${p.client}`);
645
+ if (p.agent)
646
+ fields.push(`agent: ${p.agent}`);
647
+ if (p.tags?.length)
648
+ fields.push(`tags: ${p.tags.join(', ')}`);
649
+ if (fields.length)
650
+ console.log(` ${fields.join(' ')}`);
651
+ }
652
+ console.log('');
653
+ clawck.close();
654
+ });
655
+ pattern
656
+ .command('add')
657
+ .description('Add a new tracking pattern')
658
+ .requiredOption('--name <name>', 'Pattern name')
659
+ .option('-d, --dir <path>', 'Data directory')
660
+ .option('--category <type>', 'Default category')
661
+ .option('--project <name>', 'Default project')
662
+ .option('--client <name>', 'Default client')
663
+ .option('--agent <name>', 'Default agent')
664
+ .option('--tags <tags...>', 'Default tags')
665
+ .option('--description <text>', 'Pattern description')
666
+ .action(async (opts) => {
667
+ const dataDir = resolveDataDir(opts);
668
+ const config = loadConfig(dataDir);
669
+ const configPath = path_1.default.join(path_1.default.resolve(dataDir), 'config.json');
670
+ const newPattern = { name: opts.name };
671
+ if (opts.description)
672
+ newPattern.description = opts.description;
673
+ if (opts.category)
674
+ newPattern.category = opts.category;
675
+ if (opts.project)
676
+ newPattern.project = opts.project;
677
+ if (opts.client)
678
+ newPattern.client = opts.client;
679
+ if (opts.agent)
680
+ newPattern.agent = opts.agent;
681
+ if (opts.tags)
682
+ newPattern.tags = opts.tags;
683
+ let fileConfig = {};
684
+ if (fs_1.default.existsSync(configPath)) {
685
+ try {
686
+ fileConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
687
+ }
688
+ catch { }
689
+ }
690
+ if (!fileConfig.patterns)
691
+ fileConfig.patterns = [...patterns_1.DEFAULT_PATTERNS];
692
+ fileConfig.patterns.push(newPattern);
693
+ fs_1.default.writeFileSync(configPath, JSON.stringify(fileConfig, null, 2));
694
+ console.log(` Added pattern: ${opts.name}`);
695
+ });
696
+ pattern
697
+ .command('use <name>')
698
+ .description('Set the default tracking pattern')
699
+ .option('-d, --dir <path>', 'Data directory')
700
+ .action(async (name, opts) => {
701
+ const dataDir = resolveDataDir(opts);
702
+ const configPath = path_1.default.join(path_1.default.resolve(dataDir), 'config.json');
703
+ let fileConfig = {};
704
+ if (fs_1.default.existsSync(configPath)) {
705
+ try {
706
+ fileConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
707
+ }
708
+ catch { }
709
+ }
710
+ fileConfig.default_pattern = name;
711
+ fs_1.default.writeFileSync(configPath, JSON.stringify(fileConfig, null, 2));
712
+ console.log(` Default pattern set to: ${name}`);
713
+ });
714
+ // ─── Setup ───────────────────────────────────────────────
715
+ program
716
+ .command('setup [target]')
717
+ .description('Output ready-to-paste config snippets for agent integration')
718
+ .action(async (target) => {
719
+ const snippetsDir = path_1.default.resolve(__dirname, '../../docs/snippets');
720
+ function readSnippet(filename) {
721
+ const filePath = path_1.default.join(snippetsDir, filename);
722
+ if (!fs_1.default.existsSync(filePath)) {
723
+ console.error(` Snippet not found: ${filePath}`);
724
+ process.exit(1);
725
+ }
726
+ return fs_1.default.readFileSync(filePath, 'utf-8');
727
+ }
728
+ if (!target) {
729
+ console.log(`
730
+ ⏱️🦀 Clawck Setup — Agent Integration
731
+
732
+ Available targets:
733
+
734
+ clawck setup claude Output CLAUDE.md time tracking snippet
735
+ clawck setup mcp Output MCP server config JSON
736
+ clawck setup openclaw Output OpenClaw snippets (AGENT.md + HEARTBEAT.md)
737
+
738
+ Paste the output into the appropriate file for your agent platform.
739
+ See docs/skills/clawck-setup.md for full integration guide.
740
+ `);
741
+ return;
742
+ }
743
+ switch (target) {
744
+ case 'claude': {
745
+ console.log('\n# Add this to your CLAUDE.md (project or ~/.claude/CLAUDE.md for global):\n');
746
+ console.log(readSnippet('claude-md.txt'));
747
+ break;
748
+ }
749
+ case 'mcp': {
750
+ console.log('\n# Add this to your mcp_servers.json (or ~/.claude/mcp_servers.json for global):\n');
751
+ console.log(readSnippet('mcp-config.json'));
752
+ break;
753
+ }
754
+ case 'openclaw': {
755
+ console.log('\n# ─── AGENT.md ─────────────────────────────────────────\n');
756
+ console.log(readSnippet('openclaw-agent-md.txt'));
757
+ console.log('\n# ─── HEARTBEAT.md ─────────────────────────────────────\n');
758
+ console.log(readSnippet('openclaw-heartbeat-md.txt'));
759
+ break;
760
+ }
761
+ default:
762
+ console.error(` Unknown target: "${target}". Run "clawck setup" to see options.`);
763
+ process.exit(1);
764
+ }
765
+ });
371
766
  // ─── Seed (for testing) ──────────────────────────────────
372
767
  program
373
768
  .command('seed')
374
769
  .description('Seed the database with sample entries (for testing)')
375
- .option('-d, --dir <path>', 'Data directory', '.clawck')
770
+ .option('-d, --dir <path>', 'Data directory')
376
771
  .option('-n, --count <number>', 'Number of entries', '25')
377
772
  .action(async (opts) => {
378
- const config = loadConfig(opts.dir);
773
+ const config = loadConfig(resolveDataDir(opts));
379
774
  const clawck = await new clawck_1.Clawck(config).ready();
380
775
  const count = parseInt(opts.count) || 25;
381
- const agents = ['cubi-research-01', 'cubi-writer-02', 'cubi-coder-03', 'cubi-analyst-04', 'cubi-outreach-05'];
776
+ const agents = ['research-agent-01', 'writer-agent-02', 'coder-agent-03', 'analyst-agent-04', 'outreach-agent-05'];
382
777
  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'];
778
+ const clients = ['acme-corp', 'globex-inc', 'initech', 'hooli', 'umbrella-co'];
779
+ const projects = ['website-rebuild', 'seo-content', 'grant-research', 'data-migration', 'email-campaigns', 'api-v2'];
780
+ const categories = ['research', 'content', 'code', 'data_entry', 'analysis', 'communication', 'testing', 'design', 'planning', 'other'];
386
781
  const tasks = [
387
782
  'Research competitor pricing strategies',
388
783
  'Write blog post about AI automation',
@@ -399,20 +794,26 @@ program
399
794
  'Review and optimize database queries',
400
795
  'Create investor pitch deck outline',
401
796
  'Summarize recent industry news',
797
+ 'Plan sprint roadmap for Q4',
798
+ 'Coordinate team standup notes',
402
799
  ];
800
+ const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
801
+ // Ensure coverage: cycle through all clients, projects, categories, agents
403
802
  for (let i = 0; i < count; i++) {
404
- const daysAgo = Math.random() * 14;
405
803
  const durationMin = 5 + Math.random() * 120;
406
804
  const tokensIn = Math.round(1000 + Math.random() * 50000);
407
805
  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)],
806
+ const category = i < categories.length ? categories[i] : pick(categories);
807
+ const client = i < clients.length ? clients[i] : pick(clients);
808
+ const project = i < projects.length ? projects[i] : pick(projects);
809
+ const agent = i < agents.length ? agents[i] : pick(agents);
810
+ const entry = clawck.log({
811
+ task: pick(tasks),
812
+ project,
813
+ client,
413
814
  category,
414
- agent: agents[Math.floor(Math.random() * agents.length)],
415
- model: models[Math.floor(Math.random() * models.length)],
815
+ agent,
816
+ model: pick(models),
416
817
  duration_minutes: Math.round(durationMin),
417
818
  tokens_in: tokensIn,
418
819
  tokens_out: tokensOut,
@@ -420,27 +821,82 @@ program
420
821
  summary: 'Auto-generated seed entry for testing',
421
822
  tags: ['seed', 'test'],
422
823
  });
824
+ // Approve ~60% of completed entries
825
+ if (Math.random() < 0.6) {
826
+ clawck.approve(entry.id);
827
+ }
828
+ }
829
+ // Add 2-3 running entries
830
+ for (let i = 0; i < 3; i++) {
831
+ clawck.start({
832
+ task: pick(tasks),
833
+ project: pick(projects),
834
+ client: pick(clients),
835
+ category: pick(categories),
836
+ agent: pick(agents),
837
+ model: pick(models),
838
+ tags: ['seed'],
839
+ });
423
840
  }
424
- console.log(`\n ⏱️🦀 Seeded ${count} entries into Clawck!`);
425
- console.log(` └─ Run: clawck serve\n`);
841
+ // Add 2-3 failed entries via upsert
842
+ const { v4: uuid } = await Promise.resolve().then(() => __importStar(require('uuid')));
843
+ for (let i = 0; i < 3; i++) {
844
+ const daysAgo = Math.random() * 7;
845
+ const start = new Date(Date.now() - daysAgo * 86400000);
846
+ const end = new Date(start.getTime() + (10 + Math.random() * 30) * 60000);
847
+ clawck.upsert({
848
+ id: uuid(),
849
+ task: pick(tasks),
850
+ project: pick(projects),
851
+ client: pick(clients),
852
+ category: pick(categories),
853
+ agent: pick(agents),
854
+ model: pick(models),
855
+ start: start.toISOString(),
856
+ end: end.toISOString(),
857
+ status: 'failed',
858
+ tokens_in: Math.round(500 + Math.random() * 5000),
859
+ tokens_out: Math.round(100 + Math.random() * 1000),
860
+ cost_usd: Math.round(Math.random() * 0.05 * 10000) / 10000,
861
+ tool_calls: Math.round(Math.random() * 5),
862
+ summary: 'Task failed - auto-generated seed entry',
863
+ tags: ['seed', 'failed'],
864
+ source: 'clawck',
865
+ spec_version: types_1.SPEC_VERSION,
866
+ });
867
+ }
868
+ console.log(`\n ⏱️🦀 Seeded ${count + 6} entries into Clawck!`);
869
+ console.log(` ├─ ${count} completed entries (~60% approved)`);
870
+ console.log(` ├─ 3 running entries`);
871
+ console.log(` ├─ 3 failed entries`);
872
+ console.log(` │`);
873
+ console.log(` │ Try these:`);
874
+ console.log(` │ clawck list -d ${resolveDataDir(opts)}`);
875
+ console.log(` │ clawck report --format html -d ${resolveDataDir(opts)}`);
876
+ console.log(` │ clawck pattern list -d ${resolveDataDir(opts)}`);
877
+ console.log(` │ clawck entries --approved -d ${resolveDataDir(opts)}`);
878
+ console.log(` └─ clawck serve -d ${resolveDataDir(opts)}\n`);
426
879
  clawck.close();
427
880
  });
428
881
  // ─── List ───────────────────────────────────────────────
429
882
  program
430
883
  .command('list')
431
884
  .description('List time entries in a human-readable table')
432
- .option('-d, --dir <path>', 'Data directory', '.clawck')
885
+ .option('-d, --dir <path>', 'Data directory')
433
886
  .option('--days <number>', 'Number of days to include', '7')
434
887
  .option('--client <name>', 'Filter by client')
435
888
  .option('--project <name>', 'Filter by project')
436
889
  .option('--agent <name>', 'Filter by agent')
437
890
  .option('--limit <n>', 'Max entries', '50')
891
+ .option('--approved', 'Show only approved entries')
892
+ .option('--unapproved', 'Show only unapproved entries')
438
893
  .action(async (opts) => {
439
- const config = loadConfig(opts.dir);
894
+ const config = loadConfig(resolveDataDir(opts));
440
895
  const clawck = await new clawck_1.Clawck(config).ready();
441
896
  const days = parseInt(opts.days) || 7;
442
897
  const from = new Date(Date.now() - days * 86400000).toISOString();
443
898
  const to = new Date().toISOString();
899
+ const approvedFilter = opts.approved ? true : opts.unapproved ? false : undefined;
444
900
  const entries = clawck.query({
445
901
  client: opts.client,
446
902
  project: opts.project,
@@ -448,6 +904,7 @@ program
448
904
  from,
449
905
  to,
450
906
  limit: parseInt(opts.limit) || 50,
907
+ approved: approvedFilter,
451
908
  });
452
909
  if (program.opts().json) {
453
910
  console.log(JSON.stringify(entries));
@@ -461,9 +918,9 @@ program
461
918
  program
462
919
  .command('delete <id>')
463
920
  .description('Delete a time entry by ID (supports 8-char prefix)')
464
- .option('-d, --dir <path>', 'Data directory', '.clawck')
921
+ .option('-d, --dir <path>', 'Data directory')
465
922
  .action(async (id, opts) => {
466
- const config = loadConfig(opts.dir);
923
+ const config = loadConfig(resolveDataDir(opts));
467
924
  const clawck = await new clawck_1.Clawck(config).ready();
468
925
  const entry = resolveEntryId(clawck, id);
469
926
  if (!entry) {
@@ -484,7 +941,7 @@ program
484
941
  program
485
942
  .command('edit <id>')
486
943
  .description('Edit a time entry by ID (supports 8-char prefix)')
487
- .option('-d, --dir <path>', 'Data directory', '.clawck')
944
+ .option('-d, --dir <path>', 'Data directory')
488
945
  .option('--task <text>', 'Update task description')
489
946
  .option('--duration <minutes>', 'Update duration (recalculates end)', parseFloat)
490
947
  .option('--project <name>', 'Update project')
@@ -493,7 +950,7 @@ program
493
950
  .option('--category <type>', 'Update category')
494
951
  .option('--summary <text>', 'Update summary')
495
952
  .action(async (id, opts) => {
496
- const config = loadConfig(opts.dir);
953
+ const config = loadConfig(resolveDataDir(opts));
497
954
  const clawck = await new clawck_1.Clawck(config).ready();
498
955
  const entry = resolveEntryId(clawck, id);
499
956
  if (!entry) {
@@ -532,15 +989,15 @@ program
532
989
  // ─── Export ──────────────────────────────────────────────
533
990
  program
534
991
  .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')
992
+ .description('Export time entries as JSON, CSV, or ATP envelope')
993
+ .option('-d, --dir <path>', 'Data directory')
994
+ .option('--format <type>', 'Output format (json, csv, or atp)', 'json')
538
995
  .option('--days <number>', 'Number of days to include', '7')
539
996
  .option('--client <name>', 'Filter by client')
540
997
  .option('--project <name>', 'Filter by project')
541
998
  .option('--agent <name>', 'Filter by agent')
542
999
  .action(async (opts) => {
543
- const config = loadConfig(opts.dir);
1000
+ const config = loadConfig(resolveDataDir(opts));
544
1001
  const clawck = await new clawck_1.Clawck(config).ready();
545
1002
  const days = parseInt(opts.days) || 7;
546
1003
  const from = new Date(Date.now() - days * 86400000).toISOString();
@@ -553,6 +1010,13 @@ program
553
1010
  to,
554
1011
  limit: 10000,
555
1012
  });
1013
+ if (opts.format === 'atp') {
1014
+ const baselines = clawck.getBaselines();
1015
+ const envelope = (0, atp_1.exportATP)(entries, benchmarks_1.INDUSTRY_BENCHMARKS, baselines);
1016
+ console.log(JSON.stringify(envelope, null, 2));
1017
+ clawck.close();
1018
+ return;
1019
+ }
556
1020
  if (opts.format === 'csv') {
557
1021
  const csvEscape = (val) => {
558
1022
  if (val.includes(',') || val.includes('"') || val.includes('\n')) {
@@ -560,7 +1024,7 @@ program
560
1024
  }
561
1025
  return val;
562
1026
  };
563
- console.log('id,date,task,category,duration_minutes,project,client,agent,model,status,tokens_in,tokens_out,cost_usd,summary');
1027
+ 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
1028
  for (const e of entries) {
565
1029
  const durationMin = e.end
566
1030
  ? ((new Date(e.end).getTime() - new Date(e.start).getTime()) / 60000).toFixed(2)
@@ -581,6 +1045,8 @@ program
581
1045
  String(e.tokens_out),
582
1046
  e.cost_usd.toFixed(4),
583
1047
  csvEscape(e.summary),
1048
+ csvEscape(e.created_at || ''),
1049
+ csvEscape(e.updated_at || ''),
584
1050
  ].join(','));
585
1051
  }
586
1052
  }
@@ -589,10 +1055,387 @@ program
589
1055
  }
590
1056
  clawck.close();
591
1057
  });
1058
+ // ─── Benchmark ──────────────────────────────────────────
1059
+ const benchmark = program
1060
+ .command('benchmark')
1061
+ .description('View industry benchmarks for task categories');
1062
+ benchmark
1063
+ .command('list')
1064
+ .description('List all industry benchmarks')
1065
+ .action(async () => {
1066
+ if (program.opts().json) {
1067
+ console.log(JSON.stringify(benchmarks_1.INDUSTRY_BENCHMARKS));
1068
+ return;
1069
+ }
1070
+ console.log(`\n Industry Benchmarks (human median times):`);
1071
+ console.log(` ${'─'.repeat(80)}`);
1072
+ console.log(` ${'Category'.padEnd(15)} ${'Task Type'.padEnd(25)} ${'Median'.padEnd(10)} ${'P25'.padEnd(10)} ${'P75'.padEnd(10)} Source`);
1073
+ console.log(` ${'─'.repeat(80)}`);
1074
+ for (const b of benchmarks_1.INDUSTRY_BENCHMARKS) {
1075
+ console.log(` ${b.category.padEnd(15)} ${b.task_type.padEnd(25)} ${formatDuration(b.human_median_minutes).padEnd(10)} ${formatDuration(b.human_p25_minutes).padEnd(10)} ${formatDuration(b.human_p75_minutes).padEnd(10)} ${b.source}`);
1076
+ }
1077
+ console.log('');
1078
+ });
1079
+ benchmark
1080
+ .command('category <name>')
1081
+ .description('Show benchmarks for a specific category')
1082
+ .action(async (name) => {
1083
+ const catBenchmarks = benchmarks_1.INDUSTRY_BENCHMARKS.filter(b => b.category === name);
1084
+ if (catBenchmarks.length === 0) {
1085
+ console.error(` No benchmarks found for category: ${name}`);
1086
+ console.error(` Available: ${types_2.TASK_CATEGORIES.join(', ')}`);
1087
+ process.exit(1);
1088
+ }
1089
+ if (program.opts().json) {
1090
+ console.log(JSON.stringify(catBenchmarks));
1091
+ return;
1092
+ }
1093
+ console.log(`\n Benchmarks for "${name}":`);
1094
+ for (const b of catBenchmarks) {
1095
+ console.log(` - ${b.task_type}`);
1096
+ console.log(` Median: ${formatDuration(b.human_median_minutes)} Fast: ${formatDuration(b.human_p25_minutes)} Slow: ${formatDuration(b.human_p75_minutes)}`);
1097
+ console.log(` Source: ${b.source} (${b.year})`);
1098
+ }
1099
+ console.log('');
1100
+ });
1101
+ // ─── Baseline ───────────────────────────────────────────
1102
+ const baseline = program
1103
+ .command('baseline')
1104
+ .description('Manage personal time baselines');
1105
+ baseline
1106
+ .command('add')
1107
+ .description('Add a personal baseline')
1108
+ .requiredOption('--category <type>', 'Task category')
1109
+ .requiredOption('--task-type <type>', 'Task type identifier')
1110
+ .requiredOption('--minutes <n>', 'How long this takes you (minutes)', parseFloat)
1111
+ .option('--description <text>', 'Description of the task')
1112
+ .option('-d, --dir <path>', 'Data directory')
1113
+ .action(async (opts) => {
1114
+ const config = loadConfig(resolveDataDir(opts));
1115
+ const clawck = await new clawck_1.Clawck(config).ready();
1116
+ const bl = clawck.addBaseline({
1117
+ category: opts.category,
1118
+ task_type: opts.taskType,
1119
+ description: opts.description || '',
1120
+ my_minutes: opts.minutes,
1121
+ });
1122
+ if (program.opts().json) {
1123
+ console.log(JSON.stringify(bl));
1124
+ }
1125
+ else {
1126
+ console.log(` Added baseline: ${bl.task_type} (${bl.category}) — ${formatDuration(bl.my_minutes)}`);
1127
+ console.log(` ID: ${bl.id.slice(0, 8)}`);
1128
+ }
1129
+ clawck.close();
1130
+ });
1131
+ baseline
1132
+ .command('list')
1133
+ .description('List all personal baselines')
1134
+ .option('-d, --dir <path>', 'Data directory')
1135
+ .action(async (opts) => {
1136
+ const config = loadConfig(resolveDataDir(opts));
1137
+ const clawck = await new clawck_1.Clawck(config).ready();
1138
+ const baselines = clawck.getBaselines();
1139
+ if (program.opts().json) {
1140
+ console.log(JSON.stringify(baselines));
1141
+ clawck.close();
1142
+ return;
1143
+ }
1144
+ if (baselines.length === 0) {
1145
+ console.log('\n No personal baselines set.');
1146
+ console.log(' Add one: clawck baseline add --category code --task-type code_review --minutes 30\n');
1147
+ clawck.close();
1148
+ return;
1149
+ }
1150
+ console.log(`\n Personal Baselines:`);
1151
+ for (const b of baselines) {
1152
+ console.log(` ${b.id.slice(0, 8)} ${b.category.padEnd(14)} ${b.task_type.padEnd(25)} ${formatDuration(b.my_minutes).padEnd(8)} ${b.description}`);
1153
+ }
1154
+ console.log('');
1155
+ clawck.close();
1156
+ });
1157
+ baseline
1158
+ .command('remove <id>')
1159
+ .description('Remove a personal baseline')
1160
+ .option('-d, --dir <path>', 'Data directory')
1161
+ .action(async (id, opts) => {
1162
+ const config = loadConfig(resolveDataDir(opts));
1163
+ const clawck = await new clawck_1.Clawck(config).ready();
1164
+ const deleted = clawck.removeBaseline(id);
1165
+ if (!deleted) {
1166
+ console.error(` Baseline not found: ${id}`);
1167
+ clawck.close();
1168
+ process.exit(1);
1169
+ }
1170
+ if (program.opts().json) {
1171
+ console.log(JSON.stringify({ ok: true }));
1172
+ }
1173
+ else {
1174
+ console.log(` Removed baseline: ${id}`);
1175
+ }
1176
+ clawck.close();
1177
+ });
1178
+ // ─── Demo ───────────────────────────────────────────────
1179
+ program
1180
+ .command('demo')
1181
+ .description('Generate demo data, reports, and start interactive dashboard')
1182
+ .option('-d, --dir <path>', 'Demo data directory', '.clawck-demo')
1183
+ .action(async (opts) => {
1184
+ const demoDir = path_1.default.resolve(opts.dir);
1185
+ if (!fs_1.default.existsSync(demoDir))
1186
+ fs_1.default.mkdirSync(demoDir, { recursive: true });
1187
+ const config = loadConfig(demoDir);
1188
+ config.data_dir = demoDir;
1189
+ const clawck = await new clawck_1.Clawck(config).ready();
1190
+ const { v4: uuid } = await Promise.resolve().then(() => __importStar(require('uuid')));
1191
+ const agents = ['research-agent-01', 'writer-agent-02', 'coder-agent-03', 'analyst-agent-04', 'outreach-agent-05'];
1192
+ const models = ['claude-sonnet-4-20250514', 'claude-haiku-4-5-20251001', 'gpt-4o', 'gemini-2.0-flash'];
1193
+ const clients = ['acme-corp', 'globex-inc', 'initech', 'hooli', 'umbrella-co'];
1194
+ const projects = ['website-rebuild', 'seo-content', 'grant-research', 'data-migration', 'email-campaigns', 'api-v2'];
1195
+ const categories = ['research', 'content', 'code', 'data_entry', 'analysis', 'communication', 'testing', 'design', 'planning', 'other'];
1196
+ const tasks = [
1197
+ 'Research competitor pricing strategies', 'Write blog post about AI automation',
1198
+ 'Refactor authentication module', 'Migrate legacy CSV data to new schema',
1199
+ 'Analyze Q3 customer churn patterns', 'Draft outreach emails for partnership',
1200
+ 'Write unit tests for payment flow', 'Design email template for newsletter',
1201
+ 'Find relevant grant opportunities', 'Compile industry benchmark report',
1202
+ 'Update API documentation', 'Generate social media content calendar',
1203
+ 'Review and optimize database queries', 'Create investor pitch deck outline',
1204
+ 'Summarize recent industry news', 'Plan sprint roadmap for Q4',
1205
+ ];
1206
+ const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
1207
+ // Seed 35 completed entries across 14 days
1208
+ for (let i = 0; i < 35; i++) {
1209
+ const daysAgo = Math.random() * 14;
1210
+ const durationMin = 5 + Math.random() * 120;
1211
+ const tokensIn = Math.round(1000 + Math.random() * 50000);
1212
+ const tokensOut = Math.round(500 + Math.random() * 20000);
1213
+ const toolCalls = Math.round(Math.random() * 15);
1214
+ const category = i < categories.length ? categories[i] : pick(categories);
1215
+ const end = new Date(Date.now() - daysAgo * 86400000);
1216
+ const start = new Date(end.getTime() - durationMin * 60000);
1217
+ const model = pick(models);
1218
+ clawck.upsert({
1219
+ id: uuid(),
1220
+ task: pick(tasks),
1221
+ project: i < projects.length ? projects[i] : pick(projects),
1222
+ client: i < clients.length ? clients[i] : pick(clients),
1223
+ category,
1224
+ agent: i < agents.length ? agents[i] : pick(agents),
1225
+ model,
1226
+ start: start.toISOString(),
1227
+ end: end.toISOString(),
1228
+ status: 'completed',
1229
+ tokens_in: tokensIn,
1230
+ tokens_out: tokensOut,
1231
+ cost_usd: Math.round((tokensIn * 0.000003 + tokensOut * 0.000015) * 10000) / 10000,
1232
+ tool_calls: toolCalls,
1233
+ summary: 'Demo entry',
1234
+ tags: ['demo'],
1235
+ source: 'clawck-demo',
1236
+ spec_version: '0.2.0',
1237
+ approved: Math.random() < 0.6,
1238
+ agent_runtime_ms: Math.round((tokensOut / 80) * 1000 + toolCalls * 2000),
1239
+ wall_clock_ms: Math.round(durationMin * 60000),
1240
+ });
1241
+ }
1242
+ // Add running entries
1243
+ for (let i = 0; i < 3; i++) {
1244
+ clawck.start({ task: pick(tasks), project: pick(projects), client: pick(clients), category: pick(categories), agent: pick(agents), model: pick(models), tags: ['demo'] });
1245
+ }
1246
+ // Add failed entries
1247
+ for (let i = 0; i < 3; i++) {
1248
+ const start = new Date(Date.now() - Math.random() * 7 * 86400000);
1249
+ const end = new Date(start.getTime() + (10 + Math.random() * 30) * 60000);
1250
+ clawck.upsert({
1251
+ id: uuid(), task: pick(tasks), project: pick(projects), client: pick(clients),
1252
+ category: pick(categories), agent: pick(agents), model: pick(models),
1253
+ start: start.toISOString(), end: end.toISOString(), status: 'failed',
1254
+ tokens_in: Math.round(500 + Math.random() * 5000), tokens_out: Math.round(100 + Math.random() * 1000),
1255
+ cost_usd: Math.round(Math.random() * 0.05 * 10000) / 10000, tool_calls: Math.round(Math.random() * 5),
1256
+ summary: 'Task failed', tags: ['demo', 'failed'], source: 'clawck-demo', spec_version: '0.2.0',
1257
+ });
1258
+ }
1259
+ // Add personal baselines
1260
+ const baselineData = [
1261
+ { category: 'code', task_type: 'code_review', description: 'Review a pull request', my_minutes: 30 },
1262
+ { category: 'content', task_type: 'blog_post', description: 'Write a 1000-word blog post', my_minutes: 180 },
1263
+ { category: 'research', task_type: 'competitive_analysis', description: 'Analyze competitor landscape', my_minutes: 360 },
1264
+ { category: 'testing', task_type: 'unit_tests', description: 'Write unit tests for a module', my_minutes: 120 },
1265
+ { category: 'communication', task_type: 'meeting_summary', description: 'Summarize a 1-hour meeting', my_minutes: 30 },
1266
+ { category: 'planning', task_type: 'sprint_planning', description: 'Plan a 2-week sprint', my_minutes: 120 },
1267
+ ];
1268
+ for (const bl of baselineData) {
1269
+ clawck.addBaseline(bl);
1270
+ }
1271
+ // Generate HTML report
1272
+ const to = new Date().toISOString();
1273
+ const from = new Date(Date.now() - 14 * 86400000).toISOString();
1274
+ const ts = clawck.timesheet(from, to);
1275
+ const dateRange = `${from.split('T')[0]} to ${to.split('T')[0]}`;
1276
+ const rawEntries = clawck.query({ from, to, limit: 10000 });
1277
+ const htmlPath = path_1.default.join(demoDir, 'clawck-demo-report.html');
1278
+ const html = (0, html_1.generateTimesheetHTML)(ts, { dateRange, rawEntries });
1279
+ fs_1.default.writeFileSync(htmlPath, html);
1280
+ // Generate PDF report
1281
+ const pdfPath = path_1.default.join(demoDir, 'clawck-demo-report.pdf');
1282
+ await (0, pdf_1.generateTimesheetPDF)(ts, { dateRange, outputPath: pdfPath });
1283
+ // Open HTML in browser
1284
+ const opener = process.platform === 'darwin' ? 'open' : 'xdg-open';
1285
+ const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
1286
+ try {
1287
+ execSync(`${opener} "${htmlPath}"`, { stdio: 'ignore' });
1288
+ }
1289
+ catch { }
1290
+ // Start server
1291
+ const { startServer: startDemoServer } = await Promise.resolve().then(() => __importStar(require('../server/api')));
1292
+ const net = await Promise.resolve().then(() => __importStar(require('net')));
1293
+ const port = await new Promise((resolve) => {
1294
+ const srv = net.createServer();
1295
+ srv.listen(0, () => {
1296
+ const addr = srv.address();
1297
+ srv.close(() => resolve(addr.port));
1298
+ });
1299
+ });
1300
+ config.port = port;
1301
+ console.log(`\n ⏱️🦀 Clawck Demo\n`);
1302
+ console.log(` Generated reports:`);
1303
+ console.log(` HTML: ${htmlPath} (opened in browser)`);
1304
+ console.log(` PDF: ${pdfPath}`);
1305
+ console.log(`\n Dashboard running at: http://localhost:${port}`);
1306
+ console.log(`\n Try these commands:`);
1307
+ console.log(` clawck list -d ${demoDir}`);
1308
+ console.log(` clawck report --format html -d ${demoDir}`);
1309
+ console.log(` clawck baseline list -d ${demoDir}`);
1310
+ console.log(` clawck benchmark list`);
1311
+ console.log(` clawck export --format atp -d ${demoDir}`);
1312
+ console.log(` clawck entries --approved -d ${demoDir}`);
1313
+ console.log(`\n Press Ctrl+C to stop the demo server.\n`);
1314
+ // Open dashboard
1315
+ try {
1316
+ execSync(`${opener} "http://localhost:${port}"`, { stdio: 'ignore' });
1317
+ }
1318
+ catch { }
1319
+ await startDemoServer(config);
1320
+ });
1321
+ // ─── Hook (runtime, singular) ────────────────────────────
1322
+ const hook = program
1323
+ .command('hook')
1324
+ .description('Runtime hook command (called by platform hooks, reads JSON from stdin)');
1325
+ hook
1326
+ .command('start')
1327
+ .description('Start tracking — called by platform hooks')
1328
+ .option('-d, --dir <path>', 'Data directory')
1329
+ .option('--platform <name>', 'Force platform (claude|gemini|cursor|cline|windsurf|codex)')
1330
+ .option('--verbose', 'Log received stdin JSON to stderr for debugging')
1331
+ .action(async (opts) => {
1332
+ try {
1333
+ const raw = await (0, hooks_1.readStdin)();
1334
+ if (opts.verbose)
1335
+ process.stderr.write(`clawck: hook start stdin: ${raw}\n`);
1336
+ let json = {};
1337
+ if (raw.trim()) {
1338
+ try {
1339
+ json = JSON.parse(raw);
1340
+ }
1341
+ catch {
1342
+ json = {};
1343
+ }
1344
+ }
1345
+ const platform = opts.platform || undefined;
1346
+ const context = (0, hooks_1.normalize)(json, platform);
1347
+ if (opts.verbose)
1348
+ process.stderr.write(`clawck: hook start context: ${JSON.stringify(context)}\n`);
1349
+ const config = loadConfig(resolveDataDir(opts));
1350
+ await (0, hooks_1.handleHookStart)(config, context);
1351
+ }
1352
+ catch (err) {
1353
+ const msg = err instanceof Error ? err.message : String(err);
1354
+ process.stderr.write(`clawck: hook start failed: ${msg}\n`);
1355
+ }
1356
+ process.exit(0);
1357
+ });
1358
+ hook
1359
+ .command('stop')
1360
+ .description('Stop tracking — called by platform hooks')
1361
+ .option('-d, --dir <path>', 'Data directory')
1362
+ .option('--platform <name>', 'Force platform (claude|gemini|cursor|cline|windsurf|codex)')
1363
+ .option('--verbose', 'Log received stdin JSON to stderr for debugging')
1364
+ .action(async (opts) => {
1365
+ try {
1366
+ const raw = await (0, hooks_1.readStdin)();
1367
+ if (opts.verbose)
1368
+ process.stderr.write(`clawck: hook stop stdin: ${raw}\n`);
1369
+ let json = {};
1370
+ if (raw.trim()) {
1371
+ try {
1372
+ json = JSON.parse(raw);
1373
+ }
1374
+ catch {
1375
+ json = {};
1376
+ }
1377
+ }
1378
+ const platform = opts.platform || undefined;
1379
+ const context = (0, hooks_1.normalize)(json, platform);
1380
+ if (opts.verbose)
1381
+ process.stderr.write(`clawck: hook stop context: ${JSON.stringify(context)}\n`);
1382
+ const config = loadConfig(resolveDataDir(opts));
1383
+ await (0, hooks_1.handleHookStop)(config, context);
1384
+ }
1385
+ catch (err) {
1386
+ const msg = err instanceof Error ? err.message : String(err);
1387
+ process.stderr.write(`clawck: hook stop failed: ${msg}\n`);
1388
+ }
1389
+ process.exit(0);
1390
+ });
1391
+ // ─── Hooks (management, plural) ──────────────────────────
1392
+ const hooks = program
1393
+ .command('hooks')
1394
+ .description('Manage platform hook integrations');
1395
+ hooks
1396
+ .command('install <platform>')
1397
+ .description('Output hook configuration for a platform')
1398
+ .action(async (platformName) => {
1399
+ const key = platformName.toLowerCase();
1400
+ if (!hooks_1.PLATFORMS[key]) {
1401
+ console.error(` Unknown platform: "${platformName}"`);
1402
+ console.error(` Available: ${hooks_1.PLATFORM_NAMES.join(', ')}`);
1403
+ process.exit(1);
1404
+ }
1405
+ const info = hooks_1.PLATFORMS[key];
1406
+ console.log(`\n ⏱️🦀 Clawck Hooks — ${info.displayName}`);
1407
+ console.log(` ${'─'.repeat(40)}`);
1408
+ if (info.configPaths.length > 0) {
1409
+ console.log(`\n Add to: ${info.configPaths[0]}\n`);
1410
+ }
1411
+ console.log(info.generate());
1412
+ console.log(`\n Or copy from: docs/snippets/${info.snippetFile}`);
1413
+ console.log('');
1414
+ });
1415
+ hooks
1416
+ .command('status')
1417
+ .description('Show which platforms have clawck hooks installed')
1418
+ .action(async () => {
1419
+ console.log(`\n ⏱️🦀 Clawck Hooks — Status`);
1420
+ console.log(` ${'─'.repeat(40)}`);
1421
+ for (const name of hooks_1.PLATFORM_NAMES) {
1422
+ const info = hooks_1.PLATFORMS[name];
1423
+ const installed = info.detect();
1424
+ const icon = installed ? '✓' : '✗';
1425
+ console.log(` ${icon} ${info.displayName.padEnd(16)} ${installed ? 'installed' : 'not found'}`);
1426
+ }
1427
+ console.log(`\n Run "clawck hooks install <platform>" to set up a platform.\n`);
1428
+ });
592
1429
  // ─── Helpers ──────────────────────────────────────────────
1430
+ function resolveDataDir(subcommandOpts) {
1431
+ return subcommandOpts.dir
1432
+ ?? program.opts().dir
1433
+ ?? process.env.CLAWCK_DIR
1434
+ ?? '.clawck';
1435
+ }
593
1436
  function formatDuration(mins) {
594
1437
  if (mins < 1)
595
- return '<1 min';
1438
+ return `${Math.round(mins * 60)}s`;
596
1439
  if (mins < 60)
597
1440
  return `${Math.round(mins)} min`;
598
1441
  const h = Math.floor(mins / 60);
@@ -632,7 +1475,7 @@ function printEntryTable(entries) {
632
1475
  console.log('\n No entries found.\n');
633
1476
  return;
634
1477
  }
635
- const header = ` ${'ID'.padEnd(10)} ${'Task'.padEnd(42)} ${'Duration'.padEnd(10)} ${'Project'.padEnd(12)} ${'Agent'.padEnd(12)} ${'Time'}`;
1478
+ const header = ` ${'ID'.padEnd(10)} ${'Task'.padEnd(42)} ${'Duration'.padEnd(10)} ${'Project'.padEnd(12)} ${'Agent'.padEnd(12)} ${'OK'.padEnd(4)} ${'Time'}`;
636
1479
  console.log(`\n${header}`);
637
1480
  console.log(` ${'─'.repeat(header.length - 2)}`);
638
1481
  for (const e of entries) {
@@ -642,7 +1485,8 @@ function printEntryTable(entries) {
642
1485
  const dur = e.end ? formatDuration(durationMin) : 'running';
643
1486
  const task = e.task.length > 40 ? e.task.slice(0, 37) + '...' : e.task;
644
1487
  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}`);
1488
+ const approved = e.approved ? 'v' : '-';
1489
+ 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
1490
  }
647
1491
  console.log(`\n ${entries.length} entries\n`);
648
1492
  }
@@ -682,6 +1526,22 @@ function loadConfig(dir) {
682
1526
  console.error(' Config error: "remote_sources" must be an array');
683
1527
  process.exit(1);
684
1528
  }
1529
+ if (fileConfig.webhooks !== undefined) {
1530
+ if (!Array.isArray(fileConfig.webhooks)) {
1531
+ console.error(' Config error: "webhooks" must be an array');
1532
+ process.exit(1);
1533
+ }
1534
+ for (const [i, wh] of fileConfig.webhooks.entries()) {
1535
+ if (!wh.url || typeof wh.url !== 'string') {
1536
+ console.error(` Config error: "webhooks[${i}].url" must be a string`);
1537
+ process.exit(1);
1538
+ }
1539
+ if (!Array.isArray(wh.events)) {
1540
+ console.error(` Config error: "webhooks[${i}].events" must be an array`);
1541
+ process.exit(1);
1542
+ }
1543
+ }
1544
+ }
685
1545
  return {
686
1546
  ...types_1.DEFAULT_CONFIG,
687
1547
  ...fileConfig,