clawck 0.1.0 → 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 (80) hide show
  1. package/README.md +223 -151
  2. package/dist/cli/index.js +926 -30
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/core/clawck.d.ts +16 -3
  5. package/dist/core/clawck.d.ts.map +1 -1
  6. package/dist/core/clawck.js +48 -14
  7. package/dist/core/clawck.js.map +1 -1
  8. package/dist/core/database.d.ts +4 -4
  9. package/dist/core/database.d.ts.map +1 -1
  10. package/dist/core/database.js +85 -104
  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/dashboard/index.d.ts.map +1 -1
  24. package/dist/dashboard/index.js +7 -1
  25. package/dist/dashboard/index.js.map +1 -1
  26. package/dist/hooks/adapters.d.ts +8 -0
  27. package/dist/hooks/adapters.d.ts.map +1 -0
  28. package/dist/hooks/adapters.js +138 -0
  29. package/dist/hooks/adapters.js.map +1 -0
  30. package/dist/hooks/handler.d.ts +9 -0
  31. package/dist/hooks/handler.d.ts.map +1 -0
  32. package/dist/hooks/handler.js +90 -0
  33. package/dist/hooks/handler.js.map +1 -0
  34. package/dist/hooks/index.d.ts +10 -0
  35. package/dist/hooks/index.d.ts.map +1 -0
  36. package/dist/hooks/index.js +23 -0
  37. package/dist/hooks/index.js.map +1 -0
  38. package/dist/hooks/install.d.ts +16 -0
  39. package/dist/hooks/install.d.ts.map +1 -0
  40. package/dist/hooks/install.js +239 -0
  41. package/dist/hooks/install.js.map +1 -0
  42. package/dist/hooks/session.d.ts +10 -0
  43. package/dist/hooks/session.d.ts.map +1 -0
  44. package/dist/hooks/session.js +72 -0
  45. package/dist/hooks/session.js.map +1 -0
  46. package/dist/hooks/stdin.d.ts +6 -0
  47. package/dist/hooks/stdin.d.ts.map +1 -0
  48. package/dist/hooks/stdin.js +38 -0
  49. package/dist/hooks/stdin.js.map +1 -0
  50. package/dist/hooks/types.d.ts +23 -0
  51. package/dist/hooks/types.d.ts.map +1 -0
  52. package/dist/hooks/types.js +7 -0
  53. package/dist/hooks/types.js.map +1 -0
  54. package/dist/reports/html.d.ts +13 -0
  55. package/dist/reports/html.d.ts.map +1 -0
  56. package/dist/reports/html.js +321 -0
  57. package/dist/reports/html.js.map +1 -0
  58. package/dist/reports/pdf.d.ts +13 -0
  59. package/dist/reports/pdf.d.ts.map +1 -0
  60. package/dist/reports/pdf.js +169 -0
  61. package/dist/reports/pdf.js.map +1 -0
  62. package/dist/server/api.d.ts.map +1 -1
  63. package/dist/server/api.js +18 -2
  64. package/dist/server/api.js.map +1 -1
  65. package/dist/server/mcp.d.ts.map +1 -1
  66. package/dist/server/mcp.js +140 -32
  67. package/dist/server/mcp.js.map +1 -1
  68. package/docs/skills/clawck-setup.md +131 -0
  69. package/docs/skills/clawck-usage.md +148 -0
  70. package/docs/snippets/claude-md.txt +24 -0
  71. package/docs/snippets/hooks-claude.json +16 -0
  72. package/docs/snippets/hooks-cline.txt +10 -0
  73. package/docs/snippets/hooks-codex.json +16 -0
  74. package/docs/snippets/hooks-cursor.json +16 -0
  75. package/docs/snippets/hooks-gemini.json +16 -0
  76. package/docs/snippets/hooks-windsurf.json +16 -0
  77. package/docs/snippets/mcp-config.json +7 -0
  78. package/docs/snippets/openclaw-agent-md.txt +18 -0
  79. package/docs/snippets/openclaw-heartbeat-md.txt +17 -0
  80. package/package.json +6 -3
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,18 +55,36 @@ 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.0');
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)');
30
70
  // ─── Init ─────────────────────────────────────────────────
31
71
  program
32
72
  .command('init')
33
73
  .description('Initialize a .clawck/ directory in the current folder')
34
- .option('-d, --dir <path>', 'Data directory', '.clawck')
74
+ .option('-d, --dir <path>', 'Data directory')
35
75
  .action(async (opts) => {
36
- const dir = path_1.default.resolve(opts.dir);
76
+ const resolvedDir = resolveDataDir(opts);
77
+ const dir = path_1.default.resolve(resolvedDir);
78
+ // Prevent nesting: don't create .clawck inside an existing .clawck
79
+ const cwd = process.cwd();
80
+ if (path_1.default.basename(cwd) === '.clawck' && resolvedDir === '.clawck') {
81
+ console.error(' Already inside a .clawck directory. Aborting to prevent nesting.');
82
+ process.exit(1);
83
+ }
84
+ if (path_1.default.basename(dir) === '.clawck' && fs_1.default.existsSync(path_1.default.join(dir, 'clawck.db'))) {
85
+ console.error(' Already inside a .clawck directory. Aborting to prevent nesting.');
86
+ process.exit(1);
87
+ }
37
88
  if (!fs_1.default.existsSync(dir)) {
38
89
  fs_1.default.mkdirSync(dir, { recursive: true });
39
90
  }
@@ -48,6 +99,8 @@ program
48
99
  default_source: 'clawck',
49
100
  human_equivalents: types_1.DEFAULT_HUMAN_EQUIVALENTS,
50
101
  remote_sources: [],
102
+ patterns: patterns_1.DEFAULT_PATTERNS,
103
+ default_pattern: 'default',
51
104
  };
52
105
  fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
53
106
  }
@@ -67,9 +120,9 @@ program
67
120
  .command('serve')
68
121
  .description('Start the Clawck API server and dashboard')
69
122
  .option('-p, --port <number>', 'Port number', '3456')
70
- .option('-d, --dir <path>', 'Data directory', '.clawck')
123
+ .option('-d, --dir <path>', 'Data directory')
71
124
  .action(async (opts) => {
72
- const config = loadConfig(opts.dir);
125
+ const config = loadConfig(resolveDataDir(opts));
73
126
  config.port = parseInt(opts.port) || config.port;
74
127
  (0, api_1.startServer)(config);
75
128
  });
@@ -77,21 +130,26 @@ program
77
130
  program
78
131
  .command('mcp')
79
132
  .description('Start the Clawck MCP server (stdio, for Claude Code / Cline / Cursor)')
80
- .option('-d, --dir <path>', 'Data directory', '.clawck')
133
+ .option('-d, --dir <path>', 'Data directory')
81
134
  .action(async (opts) => {
82
- const config = loadConfig(opts.dir);
135
+ const config = loadConfig(resolveDataDir(opts));
83
136
  (0, mcp_1.startMCPServer)(config);
84
137
  });
85
138
  // ─── Status ───────────────────────────────────────────────
86
139
  program
87
140
  .command('status')
88
141
  .description('Show currently running tasks and stats')
89
- .option('-d, --dir <path>', 'Data directory', '.clawck')
142
+ .option('-d, --dir <path>', 'Data directory')
90
143
  .action(async (opts) => {
91
- const config = loadConfig(opts.dir);
144
+ const config = loadConfig(resolveDataDir(opts));
92
145
  const clawck = await new clawck_1.Clawck(config).ready();
93
146
  const stats = clawck.stats();
94
147
  const running = clawck.running();
148
+ if (program.opts().json) {
149
+ console.log(JSON.stringify({ stats, running }));
150
+ clawck.close();
151
+ return;
152
+ }
95
153
  console.log(`\n ⏱️🦀 Clawck Status`);
96
154
  console.log(` ├─ Total entries: ${stats.total_entries}`);
97
155
  console.log(` ├─ Running now: ${stats.running}`);
@@ -113,13 +171,16 @@ program
113
171
  program
114
172
  .command('report')
115
173
  .description('Show a timesheet summary')
116
- .option('-d, --dir <path>', 'Data directory', '.clawck')
174
+ .option('-d, --dir <path>', 'Data directory')
117
175
  .option('--days <number>', 'Number of days to include', '7')
118
176
  .option('--client <name>', 'Filter by client')
119
177
  .option('--project <name>', 'Filter by project')
120
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)')
181
+ .option('--detailed', 'Show individual entries')
121
182
  .action(async (opts) => {
122
- const config = loadConfig(opts.dir);
183
+ const config = loadConfig(resolveDataDir(opts));
123
184
  const clawck = await new clawck_1.Clawck(config).ready();
124
185
  const days = parseInt(opts.days) || 7;
125
186
  const to = new Date().toISOString();
@@ -129,10 +190,39 @@ program
129
190
  project: opts.project,
130
191
  agent: opts.agent,
131
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
+ }
217
+ if (program.opts().json) {
218
+ console.log(JSON.stringify(ts));
219
+ clawck.close();
220
+ return;
221
+ }
132
222
  console.log(`\n 📋 Clawck Timesheet — Last ${days} days`);
133
223
  console.log(` ${'─'.repeat(50)}`);
134
- console.log(` ⏱️ Agent hours: ${ts.total_agent_hours.toFixed(1)} hrs`);
135
- console.log(` 👤 Human equiv: ${ts.total_human_equiv_hours.toFixed(1)} hrs`);
224
+ console.log(` ⏱️ Agent hours: ${ts.total_agent_hours.toFixed(2)} hrs`);
225
+ console.log(` 👤 Human equiv: ${ts.total_human_equiv_hours.toFixed(2)} hrs`);
136
226
  console.log(` 💰 Agent cost: $${ts.total_cost_usd.toFixed(2)}`);
137
227
  console.log(` 💚 Est. savings: $${ts.total_savings_usd.toFixed(0)}`);
138
228
  console.log(` 🔢 Total entries: ${ts.total_entries}`);
@@ -141,33 +231,410 @@ program
141
231
  console.log(`\n 📁 By Project:`);
142
232
  for (const p of ts.by_project) {
143
233
  const bar = '█'.repeat(Math.max(1, Math.round(p.agent_hours / (ts.total_agent_hours || 1) * 20)));
144
- console.log(` ${bar} ${p.project} (${p.client}): ${p.agent_hours.toFixed(1)}h → ${p.human_equiv_hours.toFixed(1)}h human equiv`);
234
+ console.log(` ${bar} ${p.project} (${p.client}): ${p.agent_hours.toFixed(2)}h → ${p.human_equiv_hours.toFixed(2)}h human equiv`);
145
235
  }
146
236
  }
147
237
  if (ts.by_agent.length > 0) {
148
238
  console.log(`\n 🤖 By Agent:`);
149
239
  for (const a of ts.by_agent) {
150
- console.log(` • ${a.agent} (${a.model}): ${a.agent_hours.toFixed(1)}h, ${a.success_rate}% success`);
240
+ console.log(` • ${a.agent} (${a.model}): ${a.agent_hours.toFixed(2)}h, ${a.success_rate}% success`);
241
+ }
242
+ }
243
+ if (opts.detailed) {
244
+ const entries = clawck.query({ client: opts.client, project: opts.project, agent: opts.agent, from, to, limit: 500 });
245
+ console.log(`\n 📝 Entries:`);
246
+ printEntryTable(entries);
247
+ }
248
+ console.log('');
249
+ clawck.close();
250
+ });
251
+ // ─── Start ───────────────────────────────────────────────
252
+ program
253
+ .command('start <task>')
254
+ .description('Start tracking time for a task')
255
+ .option('-d, --dir <path>', 'Data directory')
256
+ .option('--project <name>', 'Project name')
257
+ .option('--client <name>', 'Client name')
258
+ .option('--category <type>', 'Task category')
259
+ .option('--agent <name>', 'Agent name')
260
+ .option('--model <name>', 'Model name')
261
+ .option('--tags <tags...>', 'Tags')
262
+ .option('--pattern <name>', 'Use a tracking pattern')
263
+ .action(async (task, opts) => {
264
+ const config = loadConfig(resolveDataDir(opts));
265
+ const clawck = await new clawck_1.Clawck(config).ready();
266
+ const entry = clawck.start({
267
+ task,
268
+ project: opts.project,
269
+ client: opts.client,
270
+ category: opts.category,
271
+ agent: opts.agent,
272
+ model: opts.model,
273
+ tags: opts.tags,
274
+ pattern: opts.pattern,
275
+ });
276
+ if (program.opts().json) {
277
+ console.log(JSON.stringify(entry));
278
+ }
279
+ else {
280
+ console.log(` Started: ${entry.task}`);
281
+ console.log(` ID: ${entry.id}`);
282
+ console.log(` Project: ${entry.project} Client: ${entry.client}`);
283
+ }
284
+ clawck.close();
285
+ });
286
+ // ─── Stop ────────────────────────────────────────────────
287
+ program
288
+ .command('stop <id>')
289
+ .description('Stop tracking time for a task')
290
+ .option('-d, --dir <path>', 'Data directory')
291
+ .option('--status <status>', 'Outcome (completed/failed)', 'completed')
292
+ .option('--summary <text>', 'Summary of work done')
293
+ .option('--tokens-in <n>', 'Input tokens consumed', parseFloat)
294
+ .option('--tokens-out <n>', 'Output tokens generated', parseFloat)
295
+ .option('--cost <n>', 'Cost in USD', parseFloat)
296
+ .option('--tool-calls <n>', 'Number of tool calls', parseInt)
297
+ .action(async (id, opts) => {
298
+ const config = loadConfig(resolveDataDir(opts));
299
+ const clawck = await new clawck_1.Clawck(config).ready();
300
+ const entry = clawck.stop({
301
+ id,
302
+ status: opts.status,
303
+ summary: opts.summary,
304
+ tokens_in: opts.tokensIn,
305
+ tokens_out: opts.tokensOut,
306
+ cost_usd: opts.cost,
307
+ tool_calls: opts.toolCalls,
308
+ });
309
+ if (!entry) {
310
+ if (program.opts().json) {
311
+ console.log(JSON.stringify({ ok: false, error: `Entry not found: ${id}` }));
312
+ }
313
+ else {
314
+ console.error(` Entry not found: ${id}`);
315
+ }
316
+ clawck.close();
317
+ process.exit(1);
318
+ }
319
+ const duration_minutes = entry.end
320
+ ? (new Date(entry.end).getTime() - new Date(entry.start).getTime()) / 60000
321
+ : null;
322
+ if (program.opts().json) {
323
+ console.log(JSON.stringify({ ...entry, duration_minutes: duration_minutes ? Math.round(duration_minutes * 100) / 100 : null }));
324
+ }
325
+ else {
326
+ console.log(` Stopped: ${entry.task}`);
327
+ console.log(` Duration: ${duration_minutes !== null ? formatDuration(duration_minutes) : 'unknown'} Status: ${entry.status}`);
328
+ }
329
+ clawck.close();
330
+ });
331
+ // ─── Log ─────────────────────────────────────────────────
332
+ program
333
+ .command('log <task>')
334
+ .description('Log a completed task retroactively')
335
+ .option('-d, --dir <path>', 'Data directory')
336
+ .option('--duration <minutes>', 'Duration in minutes', parseFloat)
337
+ .option('--project <name>', 'Project name')
338
+ .option('--client <name>', 'Client name')
339
+ .option('--category <type>', 'Task category')
340
+ .option('--agent <name>', 'Agent name')
341
+ .option('--model <name>', 'Model name')
342
+ .option('--summary <text>', 'Summary of work done')
343
+ .option('--tags <tags...>', 'Tags')
344
+ .option('--pattern <name>', 'Use a tracking pattern')
345
+ .action(async (task, opts) => {
346
+ if (!opts.duration) {
347
+ console.error(' --duration <minutes> is required');
348
+ process.exit(1);
349
+ }
350
+ const config = loadConfig(resolveDataDir(opts));
351
+ const clawck = await new clawck_1.Clawck(config).ready();
352
+ const entry = clawck.log({
353
+ task,
354
+ duration_minutes: opts.duration,
355
+ project: opts.project,
356
+ client: opts.client,
357
+ category: opts.category,
358
+ agent: opts.agent,
359
+ model: opts.model,
360
+ summary: opts.summary,
361
+ tags: opts.tags,
362
+ pattern: opts.pattern,
363
+ });
364
+ if (program.opts().json) {
365
+ console.log(JSON.stringify(entry));
366
+ }
367
+ else {
368
+ console.log(` Logged: ${entry.task}`);
369
+ console.log(` ID: ${entry.id} Duration: ${opts.duration}m`);
370
+ }
371
+ clawck.close();
372
+ });
373
+ // ─── Get ─────────────────────────────────────────────────
374
+ program
375
+ .command('get <id>')
376
+ .description('Get a single time entry by ID')
377
+ .option('-d, --dir <path>', 'Data directory')
378
+ .action(async (id, opts) => {
379
+ const config = loadConfig(resolveDataDir(opts));
380
+ const clawck = await new clawck_1.Clawck(config).ready();
381
+ const entry = clawck.get(id);
382
+ if (!entry) {
383
+ if (program.opts().json) {
384
+ console.log(JSON.stringify({ ok: false, error: `Entry not found: ${id}` }));
385
+ }
386
+ else {
387
+ console.error(` Entry not found: ${id}`);
388
+ }
389
+ clawck.close();
390
+ process.exit(1);
391
+ }
392
+ if (program.opts().json) {
393
+ console.log(JSON.stringify(entry));
394
+ }
395
+ else {
396
+ console.log(` ${entry.task}`);
397
+ console.log(` ID: ${entry.id} Status: ${entry.status}`);
398
+ console.log(` Project: ${entry.project} Client: ${entry.client}`);
399
+ console.log(` Agent: ${entry.agent} Model: ${entry.model}`);
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}`);
403
+ }
404
+ clawck.close();
405
+ });
406
+ // ─── Entries ─────────────────────────────────────────────
407
+ program
408
+ .command('entries')
409
+ .description('Query time entries')
410
+ .option('-d, --dir <path>', 'Data directory')
411
+ .option('--client <name>', 'Filter by client')
412
+ .option('--project <name>', 'Filter by project')
413
+ .option('--agent <name>', 'Filter by agent')
414
+ .option('--status <status>', 'Filter by status')
415
+ .option('--from <date>', 'Start date (ISO 8601)')
416
+ .option('--to <date>', 'End date (ISO 8601)')
417
+ .option('--limit <n>', 'Max entries', '50')
418
+ .option('--approved', 'Show only approved entries')
419
+ .option('--unapproved', 'Show only unapproved entries')
420
+ .action(async (opts) => {
421
+ const config = loadConfig(resolveDataDir(opts));
422
+ const clawck = await new clawck_1.Clawck(config).ready();
423
+ const approvedFilter = opts.approved ? true : opts.unapproved ? false : undefined;
424
+ const entries = clawck.query({
425
+ client: opts.client,
426
+ project: opts.project,
427
+ agent: opts.agent,
428
+ status: opts.status,
429
+ from: opts.from,
430
+ to: opts.to,
431
+ limit: parseInt(opts.limit) || 50,
432
+ approved: approvedFilter,
433
+ });
434
+ if (program.opts().json) {
435
+ console.log(JSON.stringify(entries));
436
+ }
437
+ else {
438
+ console.log(`\n ${entries.length} entries:`);
439
+ for (const e of entries) {
440
+ const dur = e.end
441
+ ? `${((new Date(e.end).getTime() - new Date(e.start).getTime()) / 60000).toFixed(0)}m`
442
+ : 'running';
443
+ console.log(` ${e.id.slice(0, 8)} ${e.status.padEnd(9)} ${dur.padStart(6)} ${e.project}/${e.client} ${e.task.slice(0, 50)}`);
151
444
  }
152
445
  }
153
446
  console.log('');
154
447
  clawck.close();
155
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
+ });
156
623
  // ─── Seed (for testing) ──────────────────────────────────
157
624
  program
158
625
  .command('seed')
159
626
  .description('Seed the database with sample entries (for testing)')
160
- .option('-d, --dir <path>', 'Data directory', '.clawck')
627
+ .option('-d, --dir <path>', 'Data directory')
161
628
  .option('-n, --count <number>', 'Number of entries', '25')
162
629
  .action(async (opts) => {
163
- const config = loadConfig(opts.dir);
630
+ const config = loadConfig(resolveDataDir(opts));
164
631
  const clawck = await new clawck_1.Clawck(config).ready();
165
632
  const count = parseInt(opts.count) || 25;
166
633
  const agents = ['cubi-research-01', 'cubi-writer-02', 'cubi-coder-03', 'cubi-analyst-04', 'cubi-outreach-05'];
167
634
  const models = ['claude-sonnet-4-20250514', 'claude-haiku-4-5-20251001', 'gpt-4o', 'gemini-2.0-flash'];
168
- const clients = ['acme-corp', 'globex-inc', 'initech', 'hooli'];
169
- const projects = ['website-rebuild', 'seo-content', 'grant-research', 'data-migration', 'email-campaigns'];
170
- 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'];
171
638
  const tasks = [
172
639
  'Research competitor pricing strategies',
173
640
  'Write blog post about AI automation',
@@ -184,20 +651,26 @@ program
184
651
  'Review and optimize database queries',
185
652
  'Create investor pitch deck outline',
186
653
  'Summarize recent industry news',
654
+ 'Plan sprint roadmap for Q4',
655
+ 'Coordinate team standup notes',
187
656
  ];
657
+ const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
658
+ // Ensure coverage: cycle through all clients, projects, categories, agents
188
659
  for (let i = 0; i < count; i++) {
189
- const daysAgo = Math.random() * 14;
190
660
  const durationMin = 5 + Math.random() * 120;
191
661
  const tokensIn = Math.round(1000 + Math.random() * 50000);
192
662
  const tokensOut = Math.round(500 + Math.random() * 20000);
193
- const category = categories[Math.floor(Math.random() * categories.length)];
194
- clawck.log({
195
- task: tasks[Math.floor(Math.random() * tasks.length)],
196
- project: projects[Math.floor(Math.random() * projects.length)],
197
- 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,
198
671
  category,
199
- agent: agents[Math.floor(Math.random() * agents.length)],
200
- model: models[Math.floor(Math.random() * models.length)],
672
+ agent,
673
+ model: pick(models),
201
674
  duration_minutes: Math.round(durationMin),
202
675
  tokens_in: tokensIn,
203
676
  tokens_out: tokensOut,
@@ -205,12 +678,395 @@ program
205
678
  summary: 'Auto-generated seed entry for testing',
206
679
  tags: ['seed', 'test'],
207
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
+ });
724
+ }
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`);
736
+ clawck.close();
737
+ });
738
+ // ─── List ───────────────────────────────────────────────
739
+ program
740
+ .command('list')
741
+ .description('List time entries in a human-readable table')
742
+ .option('-d, --dir <path>', 'Data directory')
743
+ .option('--days <number>', 'Number of days to include', '7')
744
+ .option('--client <name>', 'Filter by client')
745
+ .option('--project <name>', 'Filter by project')
746
+ .option('--agent <name>', 'Filter by agent')
747
+ .option('--limit <n>', 'Max entries', '50')
748
+ .option('--approved', 'Show only approved entries')
749
+ .option('--unapproved', 'Show only unapproved entries')
750
+ .action(async (opts) => {
751
+ const config = loadConfig(resolveDataDir(opts));
752
+ const clawck = await new clawck_1.Clawck(config).ready();
753
+ const days = parseInt(opts.days) || 7;
754
+ const from = new Date(Date.now() - days * 86400000).toISOString();
755
+ const to = new Date().toISOString();
756
+ const approvedFilter = opts.approved ? true : opts.unapproved ? false : undefined;
757
+ const entries = clawck.query({
758
+ client: opts.client,
759
+ project: opts.project,
760
+ agent: opts.agent,
761
+ from,
762
+ to,
763
+ limit: parseInt(opts.limit) || 50,
764
+ approved: approvedFilter,
765
+ });
766
+ if (program.opts().json) {
767
+ console.log(JSON.stringify(entries));
768
+ clawck.close();
769
+ return;
770
+ }
771
+ printEntryTable(entries);
772
+ clawck.close();
773
+ });
774
+ // ─── Delete ─────────────────────────────────────────────
775
+ program
776
+ .command('delete <id>')
777
+ .description('Delete a time entry by ID (supports 8-char prefix)')
778
+ .option('-d, --dir <path>', 'Data directory')
779
+ .action(async (id, opts) => {
780
+ const config = loadConfig(resolveDataDir(opts));
781
+ const clawck = await new clawck_1.Clawck(config).ready();
782
+ const entry = resolveEntryId(clawck, id);
783
+ if (!entry) {
784
+ clawck.close();
785
+ process.exit(1);
786
+ }
787
+ clawck.delete(entry.id);
788
+ if (program.opts().json) {
789
+ console.log(JSON.stringify({ ok: true, deleted: entry }));
790
+ }
791
+ else {
792
+ console.log(` Deleted: ${entry.task}`);
793
+ console.log(` ID: ${entry.id.slice(0, 8)} Project: ${entry.project} Agent: ${entry.agent}`);
794
+ }
795
+ clawck.close();
796
+ });
797
+ // ─── Edit ───────────────────────────────────────────────
798
+ program
799
+ .command('edit <id>')
800
+ .description('Edit a time entry by ID (supports 8-char prefix)')
801
+ .option('-d, --dir <path>', 'Data directory')
802
+ .option('--task <text>', 'Update task description')
803
+ .option('--duration <minutes>', 'Update duration (recalculates end)', parseFloat)
804
+ .option('--project <name>', 'Update project')
805
+ .option('--client <name>', 'Update client')
806
+ .option('--agent <name>', 'Update agent')
807
+ .option('--category <type>', 'Update category')
808
+ .option('--summary <text>', 'Update summary')
809
+ .action(async (id, opts) => {
810
+ const config = loadConfig(resolveDataDir(opts));
811
+ const clawck = await new clawck_1.Clawck(config).ready();
812
+ const entry = resolveEntryId(clawck, id);
813
+ if (!entry) {
814
+ clawck.close();
815
+ process.exit(1);
816
+ }
817
+ const updates = {};
818
+ if (opts.task)
819
+ updates.task = opts.task;
820
+ if (opts.project)
821
+ updates.project = opts.project;
822
+ if (opts.client)
823
+ updates.client = opts.client;
824
+ if (opts.agent)
825
+ updates.agent = opts.agent;
826
+ if (opts.category)
827
+ updates.category = opts.category;
828
+ if (opts.summary)
829
+ updates.summary = opts.summary;
830
+ if (opts.duration !== undefined) {
831
+ updates.end = new Date(new Date(entry.start).getTime() + opts.duration * 60000).toISOString();
832
+ }
833
+ const updated = clawck.update(entry.id, updates);
834
+ if (program.opts().json) {
835
+ console.log(JSON.stringify(updated));
836
+ }
837
+ else {
838
+ console.log(` Updated: ${updated.task}`);
839
+ console.log(` ID: ${updated.id.slice(0, 8)} Project: ${updated.project} Agent: ${updated.agent}`);
840
+ if (opts.duration !== undefined) {
841
+ console.log(` Duration: ${formatDuration(opts.duration)}`);
842
+ }
843
+ }
844
+ clawck.close();
845
+ });
846
+ // ─── Export ──────────────────────────────────────────────
847
+ program
848
+ .command('export')
849
+ .description('Export time entries as JSON or CSV')
850
+ .option('-d, --dir <path>', 'Data directory')
851
+ .option('--format <type>', 'Output format (json or csv)', 'json')
852
+ .option('--days <number>', 'Number of days to include', '7')
853
+ .option('--client <name>', 'Filter by client')
854
+ .option('--project <name>', 'Filter by project')
855
+ .option('--agent <name>', 'Filter by agent')
856
+ .action(async (opts) => {
857
+ const config = loadConfig(resolveDataDir(opts));
858
+ const clawck = await new clawck_1.Clawck(config).ready();
859
+ const days = parseInt(opts.days) || 7;
860
+ const from = new Date(Date.now() - days * 86400000).toISOString();
861
+ const to = new Date().toISOString();
862
+ const entries = clawck.query({
863
+ client: opts.client,
864
+ project: opts.project,
865
+ agent: opts.agent,
866
+ from,
867
+ to,
868
+ limit: 10000,
869
+ });
870
+ if (opts.format === 'csv') {
871
+ const csvEscape = (val) => {
872
+ if (val.includes(',') || val.includes('"') || val.includes('\n')) {
873
+ return '"' + val.replace(/"/g, '""') + '"';
874
+ }
875
+ return val;
876
+ };
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');
878
+ for (const e of entries) {
879
+ const durationMin = e.end
880
+ ? ((new Date(e.end).getTime() - new Date(e.start).getTime()) / 60000).toFixed(2)
881
+ : '0';
882
+ const date = e.start.split('T')[0];
883
+ console.log([
884
+ csvEscape(e.id),
885
+ date,
886
+ csvEscape(e.task),
887
+ csvEscape(e.category),
888
+ durationMin,
889
+ csvEscape(e.project),
890
+ csvEscape(e.client),
891
+ csvEscape(e.agent),
892
+ csvEscape(e.model),
893
+ csvEscape(e.status),
894
+ String(e.tokens_in),
895
+ String(e.tokens_out),
896
+ e.cost_usd.toFixed(4),
897
+ csvEscape(e.summary),
898
+ csvEscape(e.created_at || ''),
899
+ csvEscape(e.updated_at || ''),
900
+ ].join(','));
901
+ }
902
+ }
903
+ else {
904
+ console.log(JSON.stringify(entries, null, 2));
208
905
  }
209
- console.log(`\n ⏱️🦀 Seeded ${count} entries into Clawck!`);
210
- console.log(` └─ Run: clawck serve\n`);
211
906
  clawck.close();
212
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
+ });
213
1006
  // ─── Helpers ──────────────────────────────────────────────
1007
+ function resolveDataDir(subcommandOpts) {
1008
+ return subcommandOpts.dir
1009
+ ?? program.opts().dir
1010
+ ?? process.env.CLAWCK_DIR
1011
+ ?? '.clawck';
1012
+ }
1013
+ function formatDuration(mins) {
1014
+ if (mins < 1)
1015
+ return `${Math.round(mins * 60)}s`;
1016
+ if (mins < 60)
1017
+ return `${Math.round(mins)} min`;
1018
+ const h = Math.floor(mins / 60);
1019
+ const m = Math.round(mins % 60);
1020
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
1021
+ }
1022
+ function resolveEntryId(clawck, idPrefix) {
1023
+ const exact = clawck.get(idPrefix);
1024
+ if (exact)
1025
+ return exact;
1026
+ const matches = clawck.findByPrefix(idPrefix);
1027
+ if (matches.length === 0) {
1028
+ if (program.opts().json) {
1029
+ console.log(JSON.stringify({ ok: false, error: `No entry found matching: ${idPrefix}` }));
1030
+ }
1031
+ else {
1032
+ console.error(` No entry found matching: ${idPrefix}`);
1033
+ }
1034
+ return null;
1035
+ }
1036
+ if (matches.length > 1) {
1037
+ if (program.opts().json) {
1038
+ console.log(JSON.stringify({ ok: false, error: 'Ambiguous ID prefix', matches: matches.map(e => ({ id: e.id, task: e.task })) }));
1039
+ }
1040
+ else {
1041
+ console.error(` Ambiguous ID prefix "${idPrefix}". Matches:`);
1042
+ for (const m of matches) {
1043
+ console.error(` ${m.id.slice(0, 8)} ${m.task.slice(0, 50)}`);
1044
+ }
1045
+ }
1046
+ return null;
1047
+ }
1048
+ return matches[0];
1049
+ }
1050
+ function printEntryTable(entries) {
1051
+ if (entries.length === 0) {
1052
+ console.log('\n No entries found.\n');
1053
+ return;
1054
+ }
1055
+ const header = ` ${'ID'.padEnd(10)} ${'Task'.padEnd(42)} ${'Duration'.padEnd(10)} ${'Project'.padEnd(12)} ${'Agent'.padEnd(12)} ${'OK'.padEnd(4)} ${'Time'}`;
1056
+ console.log(`\n${header}`);
1057
+ console.log(` ${'─'.repeat(header.length - 2)}`);
1058
+ for (const e of entries) {
1059
+ const durationMin = e.end
1060
+ ? (new Date(e.end).getTime() - new Date(e.start).getTime()) / 60000
1061
+ : (Date.now() - new Date(e.start).getTime()) / 60000;
1062
+ const dur = e.end ? formatDuration(durationMin) : 'running';
1063
+ const task = e.task.length > 40 ? e.task.slice(0, 37) + '...' : e.task;
1064
+ const time = new Date(e.start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
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}`);
1067
+ }
1068
+ console.log(`\n ${entries.length} entries\n`);
1069
+ }
214
1070
  function loadConfig(dir) {
215
1071
  const dataDir = path_1.default.resolve(dir);
216
1072
  const configPath = path_1.default.join(dataDir, 'config.json');
@@ -223,6 +1079,46 @@ function loadConfig(dir) {
223
1079
  // Ignore bad config
224
1080
  }
225
1081
  }
1082
+ // Validate key config fields
1083
+ if (fileConfig.port !== undefined) {
1084
+ if (typeof fileConfig.port !== 'number' || fileConfig.port < 1 || fileConfig.port > 65535) {
1085
+ console.error(` Config error: "port" must be a number between 1 and 65535 (got ${JSON.stringify(fileConfig.port)})`);
1086
+ process.exit(1);
1087
+ }
1088
+ }
1089
+ if (fileConfig.human_equivalents !== undefined) {
1090
+ if (typeof fileConfig.human_equivalents !== 'object' || fileConfig.human_equivalents === null) {
1091
+ console.error(' Config error: "human_equivalents" must be an object');
1092
+ process.exit(1);
1093
+ }
1094
+ for (const [key, val] of Object.entries(fileConfig.human_equivalents)) {
1095
+ const v = val;
1096
+ if (typeof v.multiplier !== 'number' || typeof v.human_rate_usd !== 'number') {
1097
+ console.error(` Config error: "human_equivalents.${key}" must have numeric "multiplier" and "human_rate_usd"`);
1098
+ process.exit(1);
1099
+ }
1100
+ }
1101
+ }
1102
+ if (fileConfig.remote_sources !== undefined && !Array.isArray(fileConfig.remote_sources)) {
1103
+ console.error(' Config error: "remote_sources" must be an array');
1104
+ process.exit(1);
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
+ }
226
1122
  return {
227
1123
  ...types_1.DEFAULT_CONFIG,
228
1124
  ...fileConfig,