@vibecoded/work 0.0.12 → 0.0.13
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 +33 -0
- package/dist/cli.js +280 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Minimal local time-tracking CLI (Bun + TypeScript) that stores everything on dis
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
10
|
- One command to start/stop tracking.
|
|
11
|
+
- Pause and resume active tracking.
|
|
12
|
+
- Add notes, manual entries, and undo the latest session.
|
|
11
13
|
- Task picking with fuzzy search.
|
|
12
14
|
- Monthly CSV storage for easy exports.
|
|
13
15
|
- No servers, no accounts, no lock-in.
|
|
@@ -25,6 +27,11 @@ npm install -g @vibecoded/work
|
|
|
25
27
|
work init
|
|
26
28
|
work on "Client X" "Kickoff call"
|
|
27
29
|
work status
|
|
30
|
+
work pause
|
|
31
|
+
work resume
|
|
32
|
+
work note "Investigating issue"
|
|
33
|
+
work add "Client X" "Prep" --start "2025-10-01 09" --end "2025-10-01 10" --note "offline work"
|
|
34
|
+
work undo
|
|
28
35
|
work off
|
|
29
36
|
```
|
|
30
37
|
|
|
@@ -66,6 +73,32 @@ Stop current tracking and write to the month CSV.
|
|
|
66
73
|
```typescript
|
|
67
74
|
work off
|
|
68
75
|
```
|
|
76
|
+
### `work pause`
|
|
77
|
+
Pause the current tracking session.
|
|
78
|
+
```typescript
|
|
79
|
+
work pause
|
|
80
|
+
```
|
|
81
|
+
### `work resume`
|
|
82
|
+
Resume a paused tracking session.
|
|
83
|
+
```typescript
|
|
84
|
+
work resume
|
|
85
|
+
```
|
|
86
|
+
### `work note <text>`
|
|
87
|
+
Add a note to the active session.
|
|
88
|
+
```typescript
|
|
89
|
+
work note "Investigating issue"
|
|
90
|
+
```
|
|
91
|
+
### `work add <project> <task...> [--start <date>] [--end <date>] [--note <text>]`
|
|
92
|
+
Add a manual entry. Date parts are optional; missing minutes/seconds/milliseconds are randomized.
|
|
93
|
+
```typescript
|
|
94
|
+
work add Project "Deep work" --start "2025-10-01 09" --end "2025-10-01 10"
|
|
95
|
+
work add Project "Planning" --start "2025-10-01 11:2" --end "2025-10-01 12" --note "offline"
|
|
96
|
+
```
|
|
97
|
+
### `work undo`
|
|
98
|
+
Mark the most recent session as deleted.
|
|
99
|
+
```typescript
|
|
100
|
+
work undo
|
|
101
|
+
```
|
|
69
102
|
### `work status`
|
|
70
103
|
Show current tracking (if any).
|
|
71
104
|
```typescript
|
package/dist/cli.js
CHANGED
|
@@ -2084,17 +2084,7 @@ async function ensureCsvHeader(path) {
|
|
|
2084
2084
|
}
|
|
2085
2085
|
}
|
|
2086
2086
|
async function appendCsvRow(path, row) {
|
|
2087
|
-
const line = [
|
|
2088
|
-
row.id,
|
|
2089
|
-
row.startAt,
|
|
2090
|
-
row.endAt,
|
|
2091
|
-
row.task,
|
|
2092
|
-
row.note,
|
|
2093
|
-
row.createdAt,
|
|
2094
|
-
row.updatedAt,
|
|
2095
|
-
row.deletedAt
|
|
2096
|
-
].map(esc).join(",") + `
|
|
2097
|
-
`;
|
|
2087
|
+
const line = serializeCsvRows([row], false);
|
|
2098
2088
|
await appendFile(path, line, "utf-8");
|
|
2099
2089
|
}
|
|
2100
2090
|
function unquoteCsvCell(s) {
|
|
@@ -2158,6 +2148,29 @@ async function readCsvRows(path) {
|
|
|
2158
2148
|
}
|
|
2159
2149
|
return rows;
|
|
2160
2150
|
}
|
|
2151
|
+
function serializeCsvRows(rows, includeHeader = true) {
|
|
2152
|
+
const lines = [];
|
|
2153
|
+
if (includeHeader)
|
|
2154
|
+
lines.push(HEADER.trimEnd());
|
|
2155
|
+
for (const row of rows) {
|
|
2156
|
+
lines.push([
|
|
2157
|
+
row.id,
|
|
2158
|
+
row.startAt,
|
|
2159
|
+
row.endAt,
|
|
2160
|
+
row.task,
|
|
2161
|
+
row.note,
|
|
2162
|
+
row.createdAt,
|
|
2163
|
+
row.updatedAt,
|
|
2164
|
+
row.deletedAt
|
|
2165
|
+
].map(esc).join(","));
|
|
2166
|
+
}
|
|
2167
|
+
return lines.join(`
|
|
2168
|
+
`) + `
|
|
2169
|
+
`;
|
|
2170
|
+
}
|
|
2171
|
+
async function writeCsvRows(path, rows) {
|
|
2172
|
+
await writeFile2(path, serializeCsvRows(rows, true), "utf-8");
|
|
2173
|
+
}
|
|
2161
2174
|
|
|
2162
2175
|
// src/lib/time.ts
|
|
2163
2176
|
function fmtElapsed(ms) {
|
|
@@ -2209,6 +2222,7 @@ async function saveTaskIndex(workHome, projectId, idx) {
|
|
|
2209
2222
|
}
|
|
2210
2223
|
|
|
2211
2224
|
// src/cli.ts
|
|
2225
|
+
import { readdir } from "node:fs/promises";
|
|
2212
2226
|
function usage() {
|
|
2213
2227
|
console.log(`
|
|
2214
2228
|
work - minimal time tracking (monthly CSV per project)
|
|
@@ -2217,6 +2231,11 @@ USAGE:
|
|
|
2217
2231
|
work init
|
|
2218
2232
|
work on [project] [task...]
|
|
2219
2233
|
work off
|
|
2234
|
+
work pause
|
|
2235
|
+
work resume
|
|
2236
|
+
work note <text>
|
|
2237
|
+
work add <project> <task...> [--start <date>] [--end <date>] [--note <text>]
|
|
2238
|
+
work undo
|
|
2220
2239
|
work status
|
|
2221
2240
|
work info [projectNameOrId] [YYYY-MM] [--export]
|
|
2222
2241
|
work projects
|
|
@@ -2245,6 +2264,21 @@ async function main() {
|
|
|
2245
2264
|
case "off":
|
|
2246
2265
|
await cmdOff(workHome);
|
|
2247
2266
|
return;
|
|
2267
|
+
case "pause":
|
|
2268
|
+
await cmdPause(workHome);
|
|
2269
|
+
return;
|
|
2270
|
+
case "resume":
|
|
2271
|
+
await cmdResume(workHome);
|
|
2272
|
+
return;
|
|
2273
|
+
case "note":
|
|
2274
|
+
await cmdNote(workHome, rest);
|
|
2275
|
+
return;
|
|
2276
|
+
case "add":
|
|
2277
|
+
await cmdAdd(workHome, rest, flags);
|
|
2278
|
+
return;
|
|
2279
|
+
case "undo":
|
|
2280
|
+
await cmdUndo(workHome);
|
|
2281
|
+
return;
|
|
2248
2282
|
case "status":
|
|
2249
2283
|
await cmdStatus(workHome);
|
|
2250
2284
|
return;
|
|
@@ -2362,10 +2396,118 @@ async function cmdOff(workHome) {
|
|
|
2362
2396
|
console.error("No active session.");
|
|
2363
2397
|
process.exit(1);
|
|
2364
2398
|
}
|
|
2365
|
-
|
|
2399
|
+
const endAt = running.session.pausedAt ?? nowIso();
|
|
2400
|
+
await stopAndPersist(workHome, running.session, endAt);
|
|
2366
2401
|
await saveRunning(workHome, { session: null });
|
|
2367
2402
|
console.log("Stopped.");
|
|
2368
2403
|
}
|
|
2404
|
+
async function cmdNote(workHome, rest) {
|
|
2405
|
+
await ensureWorkHome(workHome);
|
|
2406
|
+
const running = await loadRunning(workHome);
|
|
2407
|
+
if (!running.session) {
|
|
2408
|
+
console.error("No active session.");
|
|
2409
|
+
process.exit(1);
|
|
2410
|
+
}
|
|
2411
|
+
const note = rest.join(" ").trim();
|
|
2412
|
+
if (!note) {
|
|
2413
|
+
console.error("Note text is required.");
|
|
2414
|
+
process.exit(1);
|
|
2415
|
+
}
|
|
2416
|
+
running.session.note = note;
|
|
2417
|
+
await saveRunning(workHome, running);
|
|
2418
|
+
console.log("Noted.");
|
|
2419
|
+
}
|
|
2420
|
+
async function cmdAdd(workHome, rest, flags) {
|
|
2421
|
+
await ensureWorkHome(workHome);
|
|
2422
|
+
const projectName = rest[0]?.trim();
|
|
2423
|
+
const taskName = rest.slice(1).join(" ").trim();
|
|
2424
|
+
if (!projectName || !taskName) {
|
|
2425
|
+
console.error("Usage: work add <project> <task...> [--start <date>] [--end <date>] [--note <text>]");
|
|
2426
|
+
process.exit(1);
|
|
2427
|
+
}
|
|
2428
|
+
const startAt = parseDateArg(flags.start, "start");
|
|
2429
|
+
const endAt = parseDateArg(flags.end, "end");
|
|
2430
|
+
const startMs = Date.parse(startAt);
|
|
2431
|
+
const endMs = Date.parse(endAt);
|
|
2432
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) {
|
|
2433
|
+
console.error("Invalid time range. Ensure end is after start.");
|
|
2434
|
+
process.exit(1);
|
|
2435
|
+
}
|
|
2436
|
+
const projects = await loadProjects(workHome);
|
|
2437
|
+
const project = await resolveProjectByNameOrCreate(workHome, projects, projectName);
|
|
2438
|
+
const task = await resolveTaskByNameOrCreate(workHome, project.id, taskName);
|
|
2439
|
+
const idx = await loadTaskIndex(workHome, project.id);
|
|
2440
|
+
updateTaskStats(idx, task);
|
|
2441
|
+
await saveTaskIndex(workHome, project.id, idx);
|
|
2442
|
+
const ym = localYearMonth(new Date(endMs));
|
|
2443
|
+
const csvPath = getMonthCsvPath(workHome, project.id, ym);
|
|
2444
|
+
await ensureCsvHeader(csvPath);
|
|
2445
|
+
const now = nowIso();
|
|
2446
|
+
const row = {
|
|
2447
|
+
id: globalThis.crypto?.randomUUID ? globalThis.crypto.randomUUID() : randomUuidFallback(),
|
|
2448
|
+
startAt,
|
|
2449
|
+
endAt,
|
|
2450
|
+
task,
|
|
2451
|
+
note: typeof flags.note === "string" ? flags.note : "",
|
|
2452
|
+
createdAt: now,
|
|
2453
|
+
updatedAt: now,
|
|
2454
|
+
deletedAt: ""
|
|
2455
|
+
};
|
|
2456
|
+
await appendCsvRow(csvPath, row);
|
|
2457
|
+
console.log(`Added: ${formatWorkLabel(project.name, task)}`);
|
|
2458
|
+
}
|
|
2459
|
+
async function cmdPause(workHome) {
|
|
2460
|
+
await ensureWorkHome(workHome);
|
|
2461
|
+
const running = await loadRunning(workHome);
|
|
2462
|
+
if (!running.session) {
|
|
2463
|
+
console.error("No active session.");
|
|
2464
|
+
process.exit(1);
|
|
2465
|
+
}
|
|
2466
|
+
if (running.session.pausedAt) {
|
|
2467
|
+
console.log("Already paused.");
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
running.session.pausedAt = nowIso();
|
|
2471
|
+
await saveRunning(workHome, running);
|
|
2472
|
+
console.log("Paused.");
|
|
2473
|
+
}
|
|
2474
|
+
async function cmdResume(workHome) {
|
|
2475
|
+
await ensureWorkHome(workHome);
|
|
2476
|
+
const running = await loadRunning(workHome);
|
|
2477
|
+
if (!running.session) {
|
|
2478
|
+
console.error("No active session.");
|
|
2479
|
+
process.exit(1);
|
|
2480
|
+
}
|
|
2481
|
+
if (!running.session.pausedAt) {
|
|
2482
|
+
console.log("Not paused.");
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
const pausedAt = Date.parse(running.session.pausedAt);
|
|
2486
|
+
if (Number.isFinite(pausedAt)) {
|
|
2487
|
+
running.session.pausedMs = (running.session.pausedMs ?? 0) + (Date.now() - pausedAt);
|
|
2488
|
+
}
|
|
2489
|
+
running.session.pausedAt = null;
|
|
2490
|
+
await saveRunning(workHome, running);
|
|
2491
|
+
console.log("Resumed.");
|
|
2492
|
+
}
|
|
2493
|
+
async function cmdUndo(workHome) {
|
|
2494
|
+
await ensureWorkHome(workHome);
|
|
2495
|
+
const projects = await loadProjects(workHome);
|
|
2496
|
+
const latest = await findLatestSession(projects, workHome);
|
|
2497
|
+
if (!latest) {
|
|
2498
|
+
console.log("No sessions to undo.");
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
const { project, csvPath, rowIndex, rows } = latest;
|
|
2502
|
+
const now = nowIso();
|
|
2503
|
+
rows[rowIndex] = {
|
|
2504
|
+
...rows[rowIndex],
|
|
2505
|
+
updatedAt: now,
|
|
2506
|
+
deletedAt: now
|
|
2507
|
+
};
|
|
2508
|
+
await writeCsvRows(csvPath, rows);
|
|
2509
|
+
console.log(`Undone: ${formatWorkLabel(project.name, rows[rowIndex]?.task ?? "")}`);
|
|
2510
|
+
}
|
|
2369
2511
|
async function cmdStatus(workHome) {
|
|
2370
2512
|
await ensureWorkHome(workHome);
|
|
2371
2513
|
const running = await loadRunning(workHome);
|
|
@@ -2377,7 +2519,8 @@ async function cmdStatus(workHome) {
|
|
|
2377
2519
|
const elapsed = fmtElapsed(sessionElapsedMs(s));
|
|
2378
2520
|
console.log(`Running: ${formatWorkLabel(s.projectName, s.task)}
|
|
2379
2521
|
` + `Started: ${s.startAt}
|
|
2380
|
-
` + `Elapsed: ${elapsed}${s.pausedAt ? " (paused)" : ""}`
|
|
2522
|
+
` + `Elapsed: ${elapsed}${s.pausedAt ? " (paused)" : ""}` + (s.note ? `
|
|
2523
|
+
Note: ${s.note}` : ""));
|
|
2381
2524
|
}
|
|
2382
2525
|
async function cmdInfo(workHome, rest, flags) {
|
|
2383
2526
|
await ensureWorkHome(workHome);
|
|
@@ -2608,7 +2751,8 @@ function normalizeTaskLabel(task) {
|
|
|
2608
2751
|
return (task ?? "").trim() || "(no task)";
|
|
2609
2752
|
}
|
|
2610
2753
|
function sessionElapsedMs(session) {
|
|
2611
|
-
|
|
2754
|
+
const endAt = session.pausedAt ? Date.parse(session.pausedAt) : Date.now();
|
|
2755
|
+
return endAt - Date.parse(session.startAt) - (session.pausedMs ?? 0);
|
|
2612
2756
|
}
|
|
2613
2757
|
function parseInfoArgs(args) {
|
|
2614
2758
|
const a1 = args[0]?.trim();
|
|
@@ -2625,6 +2769,101 @@ function parseInfoArgs(args) {
|
|
|
2625
2769
|
}
|
|
2626
2770
|
return { projectArg, monthArg };
|
|
2627
2771
|
}
|
|
2772
|
+
function parseDateArg(value, label) {
|
|
2773
|
+
if (typeof value === "boolean") {
|
|
2774
|
+
console.error(`--${label} requires a value`);
|
|
2775
|
+
process.exit(1);
|
|
2776
|
+
}
|
|
2777
|
+
if (!value)
|
|
2778
|
+
return nowIso();
|
|
2779
|
+
const parsed = parsePartialDateTime(value);
|
|
2780
|
+
if (!parsed) {
|
|
2781
|
+
console.error(`Invalid ${label} date. Expected "YYYY-MM-DD" with optional time.`);
|
|
2782
|
+
process.exit(1);
|
|
2783
|
+
}
|
|
2784
|
+
return parsed.toISOString();
|
|
2785
|
+
}
|
|
2786
|
+
function parsePartialDateTime(input) {
|
|
2787
|
+
const trimmed = input.trim();
|
|
2788
|
+
if (!trimmed)
|
|
2789
|
+
return null;
|
|
2790
|
+
const [datePart, timePartRaw] = trimmed.split(/[T ]/);
|
|
2791
|
+
const dateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(datePart);
|
|
2792
|
+
if (!dateMatch)
|
|
2793
|
+
return null;
|
|
2794
|
+
const year = Number(dateMatch[1]);
|
|
2795
|
+
const month = Number(dateMatch[2]);
|
|
2796
|
+
const day = Number(dateMatch[3]);
|
|
2797
|
+
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day))
|
|
2798
|
+
return null;
|
|
2799
|
+
let hour = 0;
|
|
2800
|
+
let minute = randomInt(0, 59);
|
|
2801
|
+
let second = randomInt(0, 59);
|
|
2802
|
+
let ms = randomInt(0, 999);
|
|
2803
|
+
if (timePartRaw) {
|
|
2804
|
+
const timeParts = timePartRaw.split(":");
|
|
2805
|
+
if (timeParts[0])
|
|
2806
|
+
hour = Number(timeParts[0]);
|
|
2807
|
+
if (!Number.isFinite(hour))
|
|
2808
|
+
return null;
|
|
2809
|
+
if (timeParts[1] != null) {
|
|
2810
|
+
minute = fillPartialNumber(timeParts[1], 0, 59);
|
|
2811
|
+
}
|
|
2812
|
+
if (timeParts[2] != null) {
|
|
2813
|
+
const [secPart, msPart] = timeParts[2].split(".");
|
|
2814
|
+
second = fillPartialNumber(secPart, 0, 59);
|
|
2815
|
+
if (msPart != null) {
|
|
2816
|
+
ms = fillPartialMs(msPart);
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
if (timeParts[1] == null) {
|
|
2820
|
+
minute = randomInt(0, 59);
|
|
2821
|
+
second = randomInt(0, 59);
|
|
2822
|
+
ms = randomInt(0, 999);
|
|
2823
|
+
} else if (timeParts[2] == null) {
|
|
2824
|
+
second = randomInt(0, 59);
|
|
2825
|
+
ms = randomInt(0, 999);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
if (hour < 0 || hour > 23)
|
|
2829
|
+
return null;
|
|
2830
|
+
if (minute < 0 || minute > 59)
|
|
2831
|
+
return null;
|
|
2832
|
+
if (second < 0 || second > 59)
|
|
2833
|
+
return null;
|
|
2834
|
+
if (ms < 0 || ms > 999)
|
|
2835
|
+
return null;
|
|
2836
|
+
return new Date(year, month - 1, day, hour, minute, second, ms);
|
|
2837
|
+
}
|
|
2838
|
+
function fillPartialNumber(part, min, max) {
|
|
2839
|
+
const trimmed = part.trim();
|
|
2840
|
+
if (!trimmed)
|
|
2841
|
+
return randomInt(min, max);
|
|
2842
|
+
if (/^\d$/.test(trimmed)) {
|
|
2843
|
+
const tens = Number(trimmed) * 10;
|
|
2844
|
+
return clamp(tens + randomInt(0, 9), min, max);
|
|
2845
|
+
}
|
|
2846
|
+
const value = Number(trimmed);
|
|
2847
|
+
if (!Number.isFinite(value))
|
|
2848
|
+
return randomInt(min, max);
|
|
2849
|
+
return clamp(value, min, max);
|
|
2850
|
+
}
|
|
2851
|
+
function fillPartialMs(part) {
|
|
2852
|
+
const trimmed = part.trim();
|
|
2853
|
+
if (!trimmed)
|
|
2854
|
+
return randomInt(0, 999);
|
|
2855
|
+
if (/^\d{1,3}$/.test(trimmed)) {
|
|
2856
|
+
const value = Number(trimmed.padEnd(3, String(randomInt(0, 9))));
|
|
2857
|
+
return clamp(value, 0, 999);
|
|
2858
|
+
}
|
|
2859
|
+
return randomInt(0, 999);
|
|
2860
|
+
}
|
|
2861
|
+
function randomInt(min, max) {
|
|
2862
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
2863
|
+
}
|
|
2864
|
+
function clamp(value, min, max) {
|
|
2865
|
+
return Math.min(max, Math.max(min, value));
|
|
2866
|
+
}
|
|
2628
2867
|
function sortTasksByUsage(a, b) {
|
|
2629
2868
|
const ca = a.count ?? 0;
|
|
2630
2869
|
const cb = b.count ?? 0;
|
|
@@ -2660,6 +2899,33 @@ function updateTaskStats(idx, taskName) {
|
|
|
2660
2899
|
createdAt: nowIso()
|
|
2661
2900
|
});
|
|
2662
2901
|
}
|
|
2902
|
+
async function findLatestSession(projects, workHome) {
|
|
2903
|
+
let latest = null;
|
|
2904
|
+
for (const project of projects.projects) {
|
|
2905
|
+
const projectDir = getProjectDir(workHome, project.id);
|
|
2906
|
+
let entries = [];
|
|
2907
|
+
try {
|
|
2908
|
+
entries = await readdir(projectDir);
|
|
2909
|
+
} catch {
|
|
2910
|
+
continue;
|
|
2911
|
+
}
|
|
2912
|
+
const csvFiles = entries.filter((entry) => /^\d{4}-\d{2}\.csv$/.test(entry));
|
|
2913
|
+
for (const file of csvFiles) {
|
|
2914
|
+
const csvPath = pathJoin(projectDir, file);
|
|
2915
|
+
const rows = await readCsvRows(csvPath);
|
|
2916
|
+
for (let i = 0;i < rows.length; i++) {
|
|
2917
|
+
const row = rows[i];
|
|
2918
|
+
if (!row || row.deletedAt)
|
|
2919
|
+
continue;
|
|
2920
|
+
const timeMs = Date.parse(row.endAt) || Date.parse(row.startAt) || 0;
|
|
2921
|
+
if (!latest || timeMs > latest.timeMs) {
|
|
2922
|
+
latest = { project, csvPath, rowIndex: i, rows, timeMs };
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
return latest;
|
|
2928
|
+
}
|
|
2663
2929
|
process.on("uncaughtException", (error) => {
|
|
2664
2930
|
if (error && typeof error === "object" && error.name === "ExitPromptError") {
|
|
2665
2931
|
console.log("\uD83D\uDC4B until next time!");
|