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