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.
- package/README.md +223 -151
- package/dist/cli/index.js +926 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/core/clawck.d.ts +16 -3
- package/dist/core/clawck.d.ts.map +1 -1
- package/dist/core/clawck.js +48 -14
- package/dist/core/clawck.js.map +1 -1
- package/dist/core/database.d.ts +4 -4
- package/dist/core/database.d.ts.map +1 -1
- package/dist/core/database.js +85 -104
- package/dist/core/database.js.map +1 -1
- package/dist/core/patterns.d.ts +7 -0
- package/dist/core/patterns.d.ts.map +1 -0
- package/dist/core/patterns.js +36 -0
- package/dist/core/patterns.js.map +1 -0
- package/dist/core/types.d.ts +31 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/webhooks.d.ts +22 -0
- package/dist/core/webhooks.d.ts.map +1 -0
- package/dist/core/webhooks.js +70 -0
- package/dist/core/webhooks.js.map +1 -0
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +7 -1
- package/dist/dashboard/index.js.map +1 -1
- package/dist/hooks/adapters.d.ts +8 -0
- package/dist/hooks/adapters.d.ts.map +1 -0
- package/dist/hooks/adapters.js +138 -0
- package/dist/hooks/adapters.js.map +1 -0
- package/dist/hooks/handler.d.ts +9 -0
- package/dist/hooks/handler.d.ts.map +1 -0
- package/dist/hooks/handler.js +90 -0
- package/dist/hooks/handler.js.map +1 -0
- package/dist/hooks/index.d.ts +10 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +23 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/install.d.ts +16 -0
- package/dist/hooks/install.d.ts.map +1 -0
- package/dist/hooks/install.js +239 -0
- package/dist/hooks/install.js.map +1 -0
- package/dist/hooks/session.d.ts +10 -0
- package/dist/hooks/session.d.ts.map +1 -0
- package/dist/hooks/session.js +72 -0
- package/dist/hooks/session.js.map +1 -0
- package/dist/hooks/stdin.d.ts +6 -0
- package/dist/hooks/stdin.d.ts.map +1 -0
- package/dist/hooks/stdin.js +38 -0
- package/dist/hooks/stdin.js.map +1 -0
- package/dist/hooks/types.d.ts +23 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +7 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/reports/html.d.ts +13 -0
- package/dist/reports/html.d.ts.map +1 -0
- package/dist/reports/html.js +321 -0
- package/dist/reports/html.js.map +1 -0
- package/dist/reports/pdf.d.ts +13 -0
- package/dist/reports/pdf.d.ts.map +1 -0
- package/dist/reports/pdf.js +169 -0
- package/dist/reports/pdf.js.map +1 -0
- package/dist/server/api.d.ts.map +1 -1
- package/dist/server/api.js +18 -2
- package/dist/server/api.js.map +1 -1
- package/dist/server/mcp.d.ts.map +1 -1
- package/dist/server/mcp.js +140 -32
- package/dist/server/mcp.js.map +1 -1
- package/docs/skills/clawck-setup.md +131 -0
- package/docs/skills/clawck-usage.md +148 -0
- package/docs/snippets/claude-md.txt +24 -0
- package/docs/snippets/hooks-claude.json +16 -0
- package/docs/snippets/hooks-cline.txt +10 -0
- package/docs/snippets/hooks-codex.json +16 -0
- package/docs/snippets/hooks-cursor.json +16 -0
- package/docs/snippets/hooks-gemini.json +16 -0
- package/docs/snippets/hooks-windsurf.json +16 -0
- package/docs/snippets/mcp-config.json +7 -0
- package/docs/snippets/openclaw-agent-md.txt +18 -0
- package/docs/snippets/openclaw-heartbeat-md.txt +17 -0
- 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.
|
|
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'
|
|
74
|
+
.option('-d, --dir <path>', 'Data directory')
|
|
35
75
|
.action(async (opts) => {
|
|
36
|
-
const
|
|
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'
|
|
123
|
+
.option('-d, --dir <path>', 'Data directory')
|
|
71
124
|
.action(async (opts) => {
|
|
72
|
-
const config = loadConfig(opts
|
|
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'
|
|
133
|
+
.option('-d, --dir <path>', 'Data directory')
|
|
81
134
|
.action(async (opts) => {
|
|
82
|
-
const config = loadConfig(opts
|
|
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'
|
|
142
|
+
.option('-d, --dir <path>', 'Data directory')
|
|
90
143
|
.action(async (opts) => {
|
|
91
|
-
const config = loadConfig(opts
|
|
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'
|
|
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
|
|
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(
|
|
135
|
-
console.log(` 👤 Human equiv: ${ts.total_human_equiv_hours.toFixed(
|
|
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(
|
|
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(
|
|
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'
|
|
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
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
200
|
-
model:
|
|
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,
|