@vibecoded/work 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +33 -0
  2. package/dist/cli.js +280 -14
  3. 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
- await stopAndPersist(workHome, running.session, nowIso());
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
- return Date.now() - Date.parse(session.startAt) - (session.pausedMs ?? 0);
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!");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecoded/work",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Work: CLI Time Tracker",
5
5
  "module": "dist/cli.js",
6
6
  "scripts": {