@vibecoded/work 0.0.13 → 0.0.15

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 (4) hide show
  1. package/README.md +27 -5
  2. package/dist/cli.js +344 -313
  3. package/dist/lib.js +277 -0
  4. package/package.json +9 -3
package/README.md CHANGED
@@ -16,14 +16,14 @@ Minimal local time-tracking CLI (Bun + TypeScript) that stores everything on dis
16
16
 
17
17
  ## Install
18
18
 
19
- ```typescript
19
+ ```bash
20
20
  bun add -g @vibecoded/work
21
21
  npm install -g @vibecoded/work
22
22
  ````
23
23
 
24
24
  ## Quick start
25
25
 
26
- ```typescript
26
+ ```bash
27
27
  work init
28
28
  work on "Client X" "Kickoff call"
29
29
  work status
@@ -35,13 +35,35 @@ work undo
35
35
  work off
36
36
  ```
37
37
 
38
+ ## Library usage
39
+
40
+ Import the Node API and call the same lower-level helpers used by the CLI.
41
+
42
+ ```typescript
43
+ import { ensureWorkHome, getWorkHome, loadProjects } from '@vibecoded/work';
44
+
45
+ const workHome = getWorkHome();
46
+ await ensureWorkHome(workHome);
47
+
48
+ const projects = await loadProjects(workHome);
49
+ console.log(projects.projects);
50
+ ```
51
+
52
+ You can also import the explicit library entry:
53
+
54
+ ```typescript
55
+ import { fmtElapsed } from '@vibecoded/work/lib';
56
+
57
+ console.log(fmtElapsed(90_000));
58
+ ```
59
+
38
60
  ## Data location
39
61
 
40
62
  Default: `~/.work`
41
63
 
42
64
  Override with:
43
65
 
44
- ```typescript
66
+ ```bash
45
67
  work --dir ./my-work-data status
46
68
  ```
47
69
 
@@ -145,9 +167,9 @@ Each project is stored in `data/<projectId>/` with:
145
167
 
146
168
  ## Build
147
169
 
148
- Build the CLI binary:
170
+ Build the CLI and library bundles:
149
171
 
150
- ```typescript
172
+ ```bash
151
173
  bun run build
152
174
  ```
153
175
 
package/dist/cli.js CHANGED
@@ -2221,8 +2221,345 @@ async function saveTaskIndex(workHome, projectId, idx) {
2221
2221
  await saveJson(getTaskIndexPath(workHome, projectId), idx);
2222
2222
  }
2223
2223
 
2224
- // src/cli.ts
2224
+ // src/lib/services/format.ts
2225
+ function formatWorkLabel(projectName, task) {
2226
+ return `${projectName}${task ? " • " + task : ""}`;
2227
+ }
2228
+
2229
+ // src/lib/services/history.ts
2225
2230
  import { readdir } from "node:fs/promises";
2231
+ async function findLatestSession(projects, workHome) {
2232
+ let latest = null;
2233
+ for (const project of projects.projects) {
2234
+ const projectDir = getProjectDir(workHome, project.id);
2235
+ let entries = [];
2236
+ try {
2237
+ entries = await readdir(projectDir);
2238
+ } catch {
2239
+ continue;
2240
+ }
2241
+ const csvFiles = entries.filter((entry) => /^\d{4}-\d{2}\.csv$/.test(entry));
2242
+ for (const file of csvFiles) {
2243
+ const csvPath = pathJoin(projectDir, file);
2244
+ const rows = await readCsvRows(csvPath);
2245
+ for (let i = 0;i < rows.length; i++) {
2246
+ const row = rows[i];
2247
+ if (!row || row.deletedAt)
2248
+ continue;
2249
+ const timeMs = Date.parse(row.endAt) || Date.parse(row.startAt) || 0;
2250
+ if (!latest || timeMs > latest.timeMs) {
2251
+ latest = { project, csvPath, rowIndex: i, rows, timeMs };
2252
+ }
2253
+ }
2254
+ }
2255
+ }
2256
+ return latest;
2257
+ }
2258
+
2259
+ // src/lib/services/dates.ts
2260
+ function localYearMonth(d) {
2261
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}`;
2262
+ }
2263
+ function localDateKey(d) {
2264
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
2265
+ }
2266
+ function parsePartialDateTime(input) {
2267
+ const trimmed = input.trim();
2268
+ if (!trimmed)
2269
+ return null;
2270
+ const [datePart, timePartRaw] = trimmed.split(/[T ]/);
2271
+ const dateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(datePart);
2272
+ if (!dateMatch)
2273
+ return null;
2274
+ const year = Number(dateMatch[1]);
2275
+ const month = Number(dateMatch[2]);
2276
+ const day = Number(dateMatch[3]);
2277
+ if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day))
2278
+ return null;
2279
+ let hour = 0;
2280
+ let minute = randomInt(0, 59);
2281
+ let second = randomInt(0, 59);
2282
+ let ms = randomInt(0, 999);
2283
+ if (timePartRaw) {
2284
+ const timeParts = timePartRaw.split(":");
2285
+ if (timeParts[0])
2286
+ hour = Number(timeParts[0]);
2287
+ if (!Number.isFinite(hour))
2288
+ return null;
2289
+ if (timeParts[1] != null) {
2290
+ minute = fillPartialNumber(timeParts[1], 0, 59);
2291
+ }
2292
+ if (timeParts[2] != null) {
2293
+ const [secPart, msPart] = timeParts[2].split(".");
2294
+ second = fillPartialNumber(secPart, 0, 59);
2295
+ if (msPart != null) {
2296
+ ms = fillPartialMs(msPart);
2297
+ }
2298
+ }
2299
+ if (timeParts[1] == null) {
2300
+ minute = randomInt(0, 59);
2301
+ second = randomInt(0, 59);
2302
+ ms = randomInt(0, 999);
2303
+ } else if (timeParts[2] == null) {
2304
+ second = randomInt(0, 59);
2305
+ ms = randomInt(0, 999);
2306
+ }
2307
+ }
2308
+ if (hour < 0 || hour > 23)
2309
+ return null;
2310
+ if (minute < 0 || minute > 59)
2311
+ return null;
2312
+ if (second < 0 || second > 59)
2313
+ return null;
2314
+ if (ms < 0 || ms > 999)
2315
+ return null;
2316
+ return new Date(year, month - 1, day, hour, minute, second, ms);
2317
+ }
2318
+ function pad2(n) {
2319
+ return String(n).padStart(2, "0");
2320
+ }
2321
+ function fillPartialNumber(part, min, max) {
2322
+ const trimmed = part.trim();
2323
+ if (!trimmed)
2324
+ return randomInt(min, max);
2325
+ if (/^\d$/.test(trimmed)) {
2326
+ const tens = Number(trimmed) * 10;
2327
+ return clamp(tens + randomInt(0, 9), min, max);
2328
+ }
2329
+ const value = Number(trimmed);
2330
+ if (!Number.isFinite(value))
2331
+ return randomInt(min, max);
2332
+ return clamp(value, min, max);
2333
+ }
2334
+ function fillPartialMs(part) {
2335
+ const trimmed = part.trim();
2336
+ if (!trimmed)
2337
+ return randomInt(0, 999);
2338
+ if (/^\d{1,3}$/.test(trimmed)) {
2339
+ const value = Number(trimmed.padEnd(3, String(randomInt(0, 9))));
2340
+ return clamp(value, 0, 999);
2341
+ }
2342
+ return randomInt(0, 999);
2343
+ }
2344
+ function randomInt(min, max) {
2345
+ return Math.floor(Math.random() * (max - min + 1)) + min;
2346
+ }
2347
+ function clamp(value, min, max) {
2348
+ return Math.min(max, Math.max(min, value));
2349
+ }
2350
+
2351
+ // src/lib/services/info.ts
2352
+ function normalizeTaskLabel(task) {
2353
+ return (task ?? "").trim() || "(no task)";
2354
+ }
2355
+ function fmtHours(ms) {
2356
+ const totalMin = Math.round(ms / 60000);
2357
+ const h = Math.floor(totalMin / 60);
2358
+ const m = totalMin % 60;
2359
+ if (h === 0)
2360
+ return `${m}m`;
2361
+ if (m === 0)
2362
+ return `${h}h`;
2363
+ return `${h}h ${String(m).padStart(2, "0")}m`;
2364
+ }
2365
+ function buildInfoSummary(rows) {
2366
+ const active = rows.filter((r) => !r.deletedAt);
2367
+ const byDayMs = new Map;
2368
+ const byTaskMs = new Map;
2369
+ let totalMs = 0;
2370
+ for (const r of active) {
2371
+ const start = Date.parse(r.startAt);
2372
+ const end = Date.parse(r.endAt);
2373
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start)
2374
+ continue;
2375
+ const ms = end - start;
2376
+ totalMs += ms;
2377
+ const dayKey = localDateKey(new Date(start));
2378
+ byDayMs.set(dayKey, (byDayMs.get(dayKey) ?? 0) + ms);
2379
+ const taskKey = normalizeTaskLabel(r.task);
2380
+ byTaskMs.set(taskKey, (byTaskMs.get(taskKey) ?? 0) + ms);
2381
+ }
2382
+ const dayRows = sortMapByKey(byDayMs).map(([k, ms]) => ({
2383
+ key: k,
2384
+ ms,
2385
+ pretty: fmtHours(ms)
2386
+ }));
2387
+ const taskRows = sortMapByValueDesc(byTaskMs).map(([k, ms]) => ({
2388
+ key: k,
2389
+ ms,
2390
+ pretty: fmtHours(ms)
2391
+ }));
2392
+ return { totalMs, dayRows, taskRows };
2393
+ }
2394
+ function buildInfoExportCsv(projectName, month, totalMs, dayRows, taskRows) {
2395
+ const lines = [];
2396
+ lines.push(`meta,project,${csvEsc(projectName)}`);
2397
+ lines.push(`meta,month,${csvEsc(month)}`);
2398
+ lines.push(`meta,total,${csvEsc(fmtHours(totalMs))}`);
2399
+ lines.push("");
2400
+ lines.push("type,key,durationMinutes,duration");
2401
+ for (const r of dayRows) {
2402
+ lines.push(`day,${csvEsc(r.key)},${Math.round(r.ms / 60000)},${csvEsc(r.pretty)}`);
2403
+ }
2404
+ for (const r of taskRows) {
2405
+ lines.push(`task,${csvEsc(r.key)},${Math.round(r.ms / 60000)},${csvEsc(r.pretty)}`);
2406
+ }
2407
+ return lines.join(`
2408
+ `) + `
2409
+ `;
2410
+ }
2411
+ function sortMapByKey(m) {
2412
+ return [...m.entries()].sort((a, b) => a[0].localeCompare(b[0]));
2413
+ }
2414
+ function sortMapByValueDesc(m) {
2415
+ return [...m.entries()].sort((a, b) => b[1] - a[1]);
2416
+ }
2417
+ function csvEsc(v) {
2418
+ if (/[,"\n\r]/.test(v))
2419
+ return `"${v.replace(/"/g, '""')}"`;
2420
+ return v;
2421
+ }
2422
+
2423
+ // src/lib/services/ids.ts
2424
+ function createId() {
2425
+ return globalThis.crypto?.randomUUID ? globalThis.crypto.randomUUID() : randomUuidFallback();
2426
+ }
2427
+ function randomUuidFallback() {
2428
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
2429
+ const r = Math.random() * 16 | 0;
2430
+ const v = c === "x" ? r : r & 3 | 8;
2431
+ return v.toString(16);
2432
+ });
2433
+ }
2434
+
2435
+ // src/lib/services/projects.ts
2436
+ function findProjectByNameOrId(projects, arg) {
2437
+ const a = arg.trim().toLowerCase();
2438
+ return projects.projects.find((p) => p.id.toLowerCase() === a) ?? projects.projects.find((p) => p.name.toLowerCase() === a) ?? null;
2439
+ }
2440
+ async function resolveProjectByNameOrCreate(workHome, projects, name) {
2441
+ const n = name.trim();
2442
+ const found = projects.projects.find((p) => p.name.toLowerCase() === n.toLowerCase());
2443
+ if (found)
2444
+ return found;
2445
+ const created = {
2446
+ id: createId(),
2447
+ name: n,
2448
+ createdAt: nowIso()
2449
+ };
2450
+ projects.projects.push(created);
2451
+ await saveProjects(workHome, projects);
2452
+ return created;
2453
+ }
2454
+
2455
+ // src/lib/services/tasks.ts
2456
+ function sortTasksByUsage(a, b) {
2457
+ const ca = a.count ?? 0;
2458
+ const cb = b.count ?? 0;
2459
+ if (cb !== ca)
2460
+ return cb - ca;
2461
+ const la = a.lastUsedAt ? Date.parse(a.lastUsedAt) : 0;
2462
+ const lb = b.lastUsedAt ? Date.parse(b.lastUsedAt) : 0;
2463
+ if (lb !== la)
2464
+ return lb - la;
2465
+ return a.name.localeCompare(b.name);
2466
+ }
2467
+ function sortTasksByRecentUse(a, b) {
2468
+ const la = a.lastUsedAt ? Date.parse(a.lastUsedAt) : 0;
2469
+ const lb = b.lastUsedAt ? Date.parse(b.lastUsedAt) : 0;
2470
+ if (lb !== la)
2471
+ return lb - la;
2472
+ return a.name.localeCompare(b.name);
2473
+ }
2474
+ function updateTaskStats(idx, taskName) {
2475
+ const trimmed = taskName.trim();
2476
+ if (!trimmed)
2477
+ return;
2478
+ const entry = idx.tasks.find((t) => t.name.toLowerCase() === trimmed.toLowerCase());
2479
+ if (entry) {
2480
+ entry.count = (entry.count ?? 0) + 1;
2481
+ entry.lastUsedAt = nowIso();
2482
+ return;
2483
+ }
2484
+ idx.tasks.push({
2485
+ name: trimmed,
2486
+ count: 1,
2487
+ lastUsedAt: nowIso(),
2488
+ createdAt: nowIso()
2489
+ });
2490
+ }
2491
+ async function resolveTaskByNameOrCreate(workHome, projectId, task) {
2492
+ const idx = await loadTaskIndex(workHome, projectId);
2493
+ const normalized = task.trim();
2494
+ const found = idx.tasks.find((x) => x.name.toLowerCase() === normalized.toLowerCase());
2495
+ if (found)
2496
+ return found.name;
2497
+ idx.tasks.push({ name: normalized, count: 0, lastUsedAt: null, createdAt: nowIso() });
2498
+ await saveTaskIndex(workHome, projectId, idx);
2499
+ return normalized;
2500
+ }
2501
+
2502
+ // src/lib/services/sessions.ts
2503
+ function createSession(projectId, projectName, task) {
2504
+ return {
2505
+ id: createId(),
2506
+ projectId,
2507
+ projectName,
2508
+ task,
2509
+ startAt: nowIso(),
2510
+ pausedAt: null,
2511
+ pausedMs: 0,
2512
+ note: ""
2513
+ };
2514
+ }
2515
+ function sessionElapsedMs(session) {
2516
+ const endAt = session.pausedAt ? Date.parse(session.pausedAt) : Date.now();
2517
+ return endAt - Date.parse(session.startAt) - (session.pausedMs ?? 0);
2518
+ }
2519
+ async function stopAndPersist(workHome, s, endAtIso) {
2520
+ const projectDir = getProjectDir(workHome, s.projectId);
2521
+ await ensureDir(projectDir);
2522
+ const idx = await loadTaskIndex(workHome, s.projectId);
2523
+ updateTaskStats(idx, s.task);
2524
+ await saveTaskIndex(workHome, s.projectId, idx);
2525
+ const endAt = new Date(Date.parse(endAtIso));
2526
+ const ym = localYearMonth(endAt);
2527
+ const csvPath = getMonthCsvPath(workHome, s.projectId, ym);
2528
+ await ensureCsvHeader(csvPath);
2529
+ const row = {
2530
+ id: s.id,
2531
+ startAt: s.startAt,
2532
+ endAt: endAtIso,
2533
+ task: s.task,
2534
+ note: s.note ?? "",
2535
+ createdAt: s.startAt,
2536
+ updatedAt: endAtIso,
2537
+ deletedAt: ""
2538
+ };
2539
+ await appendCsvRow(csvPath, row);
2540
+ }
2541
+ async function addManualEntry(workHome, projectId, task, startAt, endAt, note) {
2542
+ const idx = await loadTaskIndex(workHome, projectId);
2543
+ updateTaskStats(idx, task);
2544
+ await saveTaskIndex(workHome, projectId, idx);
2545
+ const ym = localYearMonth(new Date(Date.parse(endAt)));
2546
+ const csvPath = getMonthCsvPath(workHome, projectId, ym);
2547
+ await ensureCsvHeader(csvPath);
2548
+ const now = nowIso();
2549
+ const row = {
2550
+ id: createId(),
2551
+ startAt,
2552
+ endAt,
2553
+ task,
2554
+ note,
2555
+ createdAt: now,
2556
+ updatedAt: now,
2557
+ deletedAt: ""
2558
+ };
2559
+ await appendCsvRow(csvPath, row);
2560
+ }
2561
+
2562
+ // src/cli.ts
2226
2563
  function usage() {
2227
2564
  console.log(`
2228
2565
  work - minimal time tracking (monthly CSV per project)
@@ -2376,16 +2713,7 @@ async function cmdOn(workHome, rest, flags) {
2376
2713
  await stopAndPersist(workHome, cur, nowIso());
2377
2714
  running.session = null;
2378
2715
  }
2379
- const session = {
2380
- id: globalThis.crypto?.randomUUID ? globalThis.crypto.randomUUID() : randomUuidFallback(),
2381
- projectId: project.id,
2382
- projectName: project.name,
2383
- task,
2384
- startAt: nowIso(),
2385
- pausedAt: null,
2386
- pausedMs: 0,
2387
- note: ""
2388
- };
2716
+ const session = createSession(project.id, project.name, task);
2389
2717
  await saveRunning(workHome, { session });
2390
2718
  console.log(`Working on ${formatWorkLabel(project.name, task)}`);
2391
2719
  }
@@ -2436,24 +2764,8 @@ async function cmdAdd(workHome, rest, flags) {
2436
2764
  const projects = await loadProjects(workHome);
2437
2765
  const project = await resolveProjectByNameOrCreate(workHome, projects, projectName);
2438
2766
  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);
2767
+ const note = typeof flags.note === "string" ? flags.note : "";
2768
+ await addManualEntry(workHome, project.id, task, startAt, endAt, note);
2457
2769
  console.log(`Added: ${formatWorkLabel(project.name, task)}`);
2458
2770
  }
2459
2771
  async function cmdPause(workHome) {
@@ -2533,34 +2845,9 @@ async function cmdInfo(workHome, rest, flags) {
2533
2845
  process.exit(1);
2534
2846
  }
2535
2847
  const projectDir = getProjectDir(workHome, project.id);
2536
- const csvPath = getMonthCsvPath(workHome, project.id, month);
2537
- const rows = await readCsvRows(csvPath);
2538
- const active = rows.filter((r) => !r.deletedAt);
2539
- const byDayMs = new Map;
2540
- const byTaskMs = new Map;
2541
- let totalMs = 0;
2542
- for (const r of active) {
2543
- const start = Date.parse(r.startAt);
2544
- const end = Date.parse(r.endAt);
2545
- if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start)
2546
- continue;
2547
- const ms = end - start;
2548
- totalMs += ms;
2549
- const dayKey = localDateKey(new Date(start));
2550
- byDayMs.set(dayKey, (byDayMs.get(dayKey) ?? 0) + ms);
2551
- const taskKey = normalizeTaskLabel(r.task);
2552
- byTaskMs.set(taskKey, (byTaskMs.get(taskKey) ?? 0) + ms);
2553
- }
2554
- const dayRows = sortMapByKey(byDayMs).map(([k, ms]) => ({
2555
- key: k,
2556
- ms,
2557
- pretty: fmtHours(ms)
2558
- }));
2559
- const taskRows = sortMapByValueDesc(byTaskMs).map(([k, ms]) => ({
2560
- key: k,
2561
- ms,
2562
- pretty: fmtHours(ms)
2563
- }));
2848
+ const csvPath = getMonthCsvPath(workHome, project.id, month);
2849
+ const rows = await readCsvRows(csvPath);
2850
+ const { totalMs, dayRows, taskRows } = buildInfoSummary(rows);
2564
2851
  console.log(`
2565
2852
  Project: ${project.name}`);
2566
2853
  console.log(`Month: ${month}`);
@@ -2580,10 +2867,6 @@ By task`);
2580
2867
  Exported: ${outPath}`);
2581
2868
  }
2582
2869
  }
2583
- function findProjectByNameOrId(projects, arg) {
2584
- const a = arg.trim().toLowerCase();
2585
- return projects.projects.find((p) => p.id.toLowerCase() === a) ?? projects.projects.find((p) => p.name.toLowerCase() === a) ?? null;
2586
- }
2587
2870
  async function pickProjectOnly(projects) {
2588
2871
  if (!projects.projects.length) {
2589
2872
  console.error("No projects found. Use: work on (create one)");
@@ -2598,20 +2881,6 @@ async function pickProjectOnly(projects) {
2598
2881
  });
2599
2882
  return projects.projects.find((p) => p.id === picked) ?? null;
2600
2883
  }
2601
- async function resolveProjectByNameOrCreate(workHome, projects, name) {
2602
- const n = name.trim();
2603
- const found = projects.projects.find((p) => p.name.toLowerCase() === n.toLowerCase());
2604
- if (found)
2605
- return found;
2606
- const created = {
2607
- id: globalThis.crypto?.randomUUID ? globalThis.crypto.randomUUID() : randomUuidFallback(),
2608
- name: n,
2609
- createdAt: nowIso()
2610
- };
2611
- projects.projects.push(created);
2612
- await saveProjects(workHome, projects);
2613
- return created;
2614
- }
2615
2884
  async function pickProjectOrCreate(workHome, projects) {
2616
2885
  const picked = await dist_default4({
2617
2886
  message: "Select a project",
@@ -2637,16 +2906,6 @@ async function pickProjectOrCreate(workHome, projects) {
2637
2906
  throw new Error("Project selection failed");
2638
2907
  return found;
2639
2908
  }
2640
- async function resolveTaskByNameOrCreate(workHome, projectId, task) {
2641
- const idx = await loadTaskIndex(workHome, projectId);
2642
- const normalized = task.trim();
2643
- const found = idx.tasks.find((x) => x.name.toLowerCase() === normalized.toLowerCase());
2644
- if (found)
2645
- return found.name;
2646
- idx.tasks.push({ name: normalized, count: 0, lastUsedAt: null, createdAt: nowIso() });
2647
- await saveTaskIndex(workHome, projectId, idx);
2648
- return normalized;
2649
- }
2650
2909
  async function pickTaskOrCreate(workHome, projectId, projectName) {
2651
2910
  const idx = await loadTaskIndex(workHome, projectId);
2652
2911
  const picked = await dist_default4({
@@ -2667,53 +2926,6 @@ async function pickTaskOrCreate(workHome, projectId, projectName) {
2667
2926
  }
2668
2927
  return picked;
2669
2928
  }
2670
- async function stopAndPersist(workHome, s, endAtIso) {
2671
- const projectDir = getProjectDir(workHome, s.projectId);
2672
- await ensureDir(projectDir);
2673
- const idx = await loadTaskIndex(workHome, s.projectId);
2674
- updateTaskStats(idx, s.task);
2675
- await saveTaskIndex(workHome, s.projectId, idx);
2676
- const endAt = new Date(Date.parse(endAtIso));
2677
- const ym = localYearMonth(endAt);
2678
- const csvPath = getMonthCsvPath(workHome, s.projectId, ym);
2679
- await ensureCsvHeader(csvPath);
2680
- const row = {
2681
- id: s.id,
2682
- startAt: s.startAt,
2683
- endAt: endAtIso,
2684
- task: s.task,
2685
- note: s.note ?? "",
2686
- createdAt: s.startAt,
2687
- updatedAt: endAtIso,
2688
- deletedAt: ""
2689
- };
2690
- await appendCsvRow(csvPath, row);
2691
- }
2692
- function pad2(n) {
2693
- return String(n).padStart(2, "0");
2694
- }
2695
- function localYearMonth(d) {
2696
- return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}`;
2697
- }
2698
- function localDateKey(d) {
2699
- return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
2700
- }
2701
- function fmtHours(ms) {
2702
- const totalMin = Math.round(ms / 60000);
2703
- const h = Math.floor(totalMin / 60);
2704
- const m = totalMin % 60;
2705
- if (h === 0)
2706
- return `${m}m`;
2707
- if (m === 0)
2708
- return `${h}h`;
2709
- return `${h}h ${String(m).padStart(2, "0")}m`;
2710
- }
2711
- function sortMapByKey(m) {
2712
- return [...m.entries()].sort((a, b) => a[0].localeCompare(b[0]));
2713
- }
2714
- function sortMapByValueDesc(m) {
2715
- return [...m.entries()].sort((a, b) => b[1] - a[1]);
2716
- }
2717
2929
  function printTable(headers, rows) {
2718
2930
  const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
2719
2931
  const line = (cols) => cols.map((c, i) => (c ?? "").padEnd(widths[i] || 0)).join(" ").trimEnd();
@@ -2722,38 +2934,6 @@ function printTable(headers, rows) {
2722
2934
  for (const r of rows)
2723
2935
  console.log(line(r));
2724
2936
  }
2725
- function buildInfoExportCsv(projectName, month, totalMs, dayRows, taskRows) {
2726
- const lines = [];
2727
- lines.push(`meta,project,${csvEsc(projectName)}`);
2728
- lines.push(`meta,month,${csvEsc(month)}`);
2729
- lines.push(`meta,total,${csvEsc(fmtHours(totalMs))}`);
2730
- lines.push("");
2731
- lines.push("type,key,durationMinutes,duration");
2732
- for (const r of dayRows) {
2733
- lines.push(`day,${csvEsc(r.key)},${Math.round(r.ms / 60000)},${csvEsc(r.pretty)}`);
2734
- }
2735
- for (const r of taskRows) {
2736
- lines.push(`task,${csvEsc(r.key)},${Math.round(r.ms / 60000)},${csvEsc(r.pretty)}`);
2737
- }
2738
- return lines.join(`
2739
- `) + `
2740
- `;
2741
- }
2742
- function csvEsc(v) {
2743
- if (/[,"\n\r]/.test(v))
2744
- return `"${v.replace(/"/g, '""')}"`;
2745
- return v;
2746
- }
2747
- function formatWorkLabel(projectName, task) {
2748
- return `${projectName}${task ? " • " + task : ""}`;
2749
- }
2750
- function normalizeTaskLabel(task) {
2751
- return (task ?? "").trim() || "(no task)";
2752
- }
2753
- function sessionElapsedMs(session) {
2754
- const endAt = session.pausedAt ? Date.parse(session.pausedAt) : Date.now();
2755
- return endAt - Date.parse(session.startAt) - (session.pausedMs ?? 0);
2756
- }
2757
2937
  function parseInfoArgs(args) {
2758
2938
  const a1 = args[0]?.trim();
2759
2939
  const a2 = args[1]?.trim();
@@ -2783,149 +2963,6 @@ function parseDateArg(value, label) {
2783
2963
  }
2784
2964
  return parsed.toISOString();
2785
2965
  }
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
- }
2867
- function sortTasksByUsage(a, b) {
2868
- const ca = a.count ?? 0;
2869
- const cb = b.count ?? 0;
2870
- if (cb !== ca)
2871
- return cb - ca;
2872
- const la = a.lastUsedAt ? Date.parse(a.lastUsedAt) : 0;
2873
- const lb = b.lastUsedAt ? Date.parse(b.lastUsedAt) : 0;
2874
- if (lb !== la)
2875
- return lb - la;
2876
- return a.name.localeCompare(b.name);
2877
- }
2878
- function sortTasksByRecentUse(a, b) {
2879
- const la = a.lastUsedAt ? Date.parse(a.lastUsedAt) : 0;
2880
- const lb = b.lastUsedAt ? Date.parse(b.lastUsedAt) : 0;
2881
- if (lb !== la)
2882
- return lb - la;
2883
- return a.name.localeCompare(b.name);
2884
- }
2885
- function updateTaskStats(idx, taskName) {
2886
- const trimmed = taskName.trim();
2887
- if (!trimmed)
2888
- return;
2889
- const entry = idx.tasks.find((t) => t.name.toLowerCase() === trimmed.toLowerCase());
2890
- if (entry) {
2891
- entry.count = (entry.count ?? 0) + 1;
2892
- entry.lastUsedAt = nowIso();
2893
- return;
2894
- }
2895
- idx.tasks.push({
2896
- name: trimmed,
2897
- count: 1,
2898
- lastUsedAt: nowIso(),
2899
- createdAt: nowIso()
2900
- });
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
- }
2929
2966
  process.on("uncaughtException", (error) => {
2930
2967
  if (error && typeof error === "object" && error.name === "ExitPromptError") {
2931
2968
  console.log("\uD83D\uDC4B until next time!");
@@ -2933,10 +2970,4 @@ process.on("uncaughtException", (error) => {
2933
2970
  throw error;
2934
2971
  }
2935
2972
  });
2936
- function randomUuidFallback() {
2937
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
2938
- const r = Math.random() * 16 | 0, v = c === "x" ? r : r & 3 | 8;
2939
- return v.toString(16);
2940
- });
2941
- }
2942
2973
  await main();
package/dist/lib.js ADDED
@@ -0,0 +1,277 @@
1
+ // src/lib/csv.ts
2
+ import { appendFile, readFile, writeFile, access } from "node:fs/promises";
3
+ import { constants as FS } from "node:fs";
4
+ var HEADER = `id,startAt,endAt,task,note,createdAt,updatedAt,deletedAt
5
+ `;
6
+ function esc(v) {
7
+ const s = (v ?? "").toString();
8
+ if (/[,"\n\r]/.test(s))
9
+ return `"${s.replace(/"/g, '""')}"`;
10
+ return s;
11
+ }
12
+ async function exists(path) {
13
+ try {
14
+ await access(path, FS.F_OK);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+ async function ensureCsvHeader(path) {
21
+ if (!await exists(path)) {
22
+ await writeFile(path, HEADER, "utf-8");
23
+ }
24
+ }
25
+ async function appendCsvRow(path, row) {
26
+ const line = serializeCsvRows([row], false);
27
+ await appendFile(path, line, "utf-8");
28
+ }
29
+ function unquoteCsvCell(s) {
30
+ if (s.startsWith('"') && s.endsWith('"')) {
31
+ return s.slice(1, -1).replace(/""/g, '"');
32
+ }
33
+ return s;
34
+ }
35
+ function splitCsvLine(line) {
36
+ const out = [];
37
+ let cur = "";
38
+ let inQ = false;
39
+ for (let i = 0;i < line.length; i++) {
40
+ const ch = line[i];
41
+ if (ch === '"') {
42
+ const next = line[i + 1];
43
+ if (inQ && next === '"') {
44
+ cur += '"';
45
+ i++;
46
+ } else {
47
+ inQ = !inQ;
48
+ }
49
+ continue;
50
+ }
51
+ if (ch === "," && !inQ) {
52
+ out.push(cur);
53
+ cur = "";
54
+ continue;
55
+ }
56
+ cur += ch;
57
+ }
58
+ out.push(cur);
59
+ return out.map(unquoteCsvCell);
60
+ }
61
+ async function readCsvRows(path) {
62
+ if (!await exists(path))
63
+ return [];
64
+ const txt = await readFile(path, "utf-8");
65
+ const lines = txt.split(/\r?\n/).filter((l) => l.trim().length > 0);
66
+ if (lines.length <= 1)
67
+ return [];
68
+ const header = lines[0]?.trim();
69
+ if (header !== HEADER.trim()) {
70
+ throw new Error(`Unexpected CSV header in ${path}`);
71
+ }
72
+ const rows = [];
73
+ for (let i = 1;i < lines.length; i++) {
74
+ const cols = splitCsvLine(lines[i] || "");
75
+ if (cols.length < 8)
76
+ continue;
77
+ rows.push({
78
+ id: cols[0] ?? "",
79
+ startAt: cols[1] ?? "",
80
+ endAt: cols[2] ?? "",
81
+ task: cols[3] ?? "",
82
+ note: cols[4] ?? "",
83
+ createdAt: cols[5] ?? "",
84
+ updatedAt: cols[6] ?? "",
85
+ deletedAt: cols[7] ?? ""
86
+ });
87
+ }
88
+ return rows;
89
+ }
90
+ function serializeCsvRows(rows, includeHeader = true) {
91
+ const lines = [];
92
+ if (includeHeader)
93
+ lines.push(HEADER.trimEnd());
94
+ for (const row of rows) {
95
+ lines.push([
96
+ row.id,
97
+ row.startAt,
98
+ row.endAt,
99
+ row.task,
100
+ row.note,
101
+ row.createdAt,
102
+ row.updatedAt,
103
+ row.deletedAt
104
+ ].map(esc).join(","));
105
+ }
106
+ return lines.join(`
107
+ `) + `
108
+ `;
109
+ }
110
+ async function writeCsvRows(path, rows) {
111
+ await writeFile(path, serializeCsvRows(rows, true), "utf-8");
112
+ }
113
+ // src/lib/fs.ts
114
+ import { mkdir } from "node:fs/promises";
115
+ import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
116
+ import { homedir } from "node:os";
117
+ function pathJoin(...parts) {
118
+ const cleaned = parts.filter(Boolean).map((p, i) => {
119
+ let s = String(p).trim();
120
+ if (i === 0)
121
+ s = s.replace(/\/+$/g, "");
122
+ else
123
+ s = s.replace(/^\/+/g, "").replace(/\/+$/g, "");
124
+ return s;
125
+ }).filter((p) => p.length > 0);
126
+ if (cleaned.length === 0)
127
+ return "";
128
+ const joined = cleaned.join("/");
129
+ const first = parts.find((p) => String(p ?? "").trim().length > 0) ?? "";
130
+ const isAbs = String(first).trim().startsWith("/");
131
+ return isAbs ? joined.startsWith("/") ? joined : `/${joined}` : joined;
132
+ }
133
+ function expandHome(p) {
134
+ const home = process.env.HOME || process.env.USERPROFILE || homedir() || "";
135
+ if (!home)
136
+ return p;
137
+ if (p === "~")
138
+ return home;
139
+ if (p.startsWith("~/"))
140
+ return `${home}/${p.slice(2)}`;
141
+ return p;
142
+ }
143
+ function getWorkHome(dirFlag) {
144
+ if (typeof dirFlag === "string" && dirFlag.trim())
145
+ return expandHome(dirFlag.trim());
146
+ const home = process.env.HOME || process.env.USERPROFILE || homedir() || ".";
147
+ return pathJoin(home, ".work");
148
+ }
149
+ async function ensureDir(dir) {
150
+ await mkdir(dir, { recursive: true });
151
+ }
152
+ async function ensureWorkHome(workHome) {
153
+ await ensureDir(workHome);
154
+ await ensureDir(pathJoin(workHome, "data"));
155
+ }
156
+ async function loadJson(path, fallback) {
157
+ try {
158
+ const txt = await readFile2(path, "utf8");
159
+ return JSON.parse(txt);
160
+ } catch {
161
+ return fallback;
162
+ }
163
+ }
164
+ async function saveJson(path, data) {
165
+ const txt = JSON.stringify(data, null, 2) + `
166
+ `;
167
+ await writeFile2(path, txt, "utf8");
168
+ }
169
+ async function writeTextFile(path, content) {
170
+ await writeFile2(path, content, "utf8");
171
+ }
172
+ function nowIso() {
173
+ return new Date().toISOString();
174
+ }
175
+ function slugify(s) {
176
+ return s.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "project";
177
+ }
178
+ function parseArgs(argv) {
179
+ const flags = {};
180
+ const positional = [];
181
+ for (let i = 0;i < argv.length; i++) {
182
+ const a = String(argv[i] ?? "");
183
+ if (a.startsWith("--")) {
184
+ const key = a.slice(2);
185
+ const next = argv[i + 1];
186
+ if (!next || String(next).startsWith("-")) {
187
+ flags[key] = true;
188
+ } else {
189
+ flags[key] = String(next);
190
+ i++;
191
+ }
192
+ } else {
193
+ positional.push(a);
194
+ }
195
+ }
196
+ const cmd = positional[0];
197
+ const rest = positional.slice(1);
198
+ return { cmd, rest, flags };
199
+ }
200
+ // src/lib/store.ts
201
+ var PROJECTS_FILE = "projects.json";
202
+ var RUNNING_FILE = "running.json";
203
+ var TASKS_FILE = "tasks.json";
204
+ function getProjectsPath(workHome) {
205
+ return pathJoin(workHome, PROJECTS_FILE);
206
+ }
207
+ function getRunningPath(workHome) {
208
+ return pathJoin(workHome, RUNNING_FILE);
209
+ }
210
+ function getProjectDir(workHome, projectId) {
211
+ return pathJoin(workHome, "data", projectId);
212
+ }
213
+ function getTaskIndexPath(workHome, projectId) {
214
+ return pathJoin(getProjectDir(workHome, projectId), TASKS_FILE);
215
+ }
216
+ function getMonthCsvPath(workHome, projectId, yearMonth) {
217
+ return pathJoin(getProjectDir(workHome, projectId), `${yearMonth}.csv`);
218
+ }
219
+ async function loadProjects(workHome) {
220
+ return loadJson(getProjectsPath(workHome), { projects: [] });
221
+ }
222
+ async function saveProjects(workHome, projects) {
223
+ await saveJson(getProjectsPath(workHome), projects);
224
+ }
225
+ async function loadRunning(workHome) {
226
+ return loadJson(getRunningPath(workHome), { session: null });
227
+ }
228
+ async function saveRunning(workHome, running) {
229
+ await saveJson(getRunningPath(workHome), running);
230
+ }
231
+ async function loadTaskIndex(workHome, projectId) {
232
+ await ensureDir(getProjectDir(workHome, projectId));
233
+ return loadJson(getTaskIndexPath(workHome, projectId), { tasks: [] });
234
+ }
235
+ async function saveTaskIndex(workHome, projectId, idx) {
236
+ await ensureDir(getProjectDir(workHome, projectId));
237
+ await saveJson(getTaskIndexPath(workHome, projectId), idx);
238
+ }
239
+ // src/lib/time.ts
240
+ function fmtElapsed(ms) {
241
+ const total = Math.max(0, Math.floor(ms / 1000));
242
+ const h = Math.floor(total / 3600);
243
+ const m = Math.floor(total % 3600 / 60);
244
+ const s = total % 60;
245
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
246
+ }
247
+ export {
248
+ writeTextFile,
249
+ writeCsvRows,
250
+ slugify,
251
+ serializeCsvRows,
252
+ saveTaskIndex,
253
+ saveRunning,
254
+ saveProjects,
255
+ saveJson,
256
+ readCsvRows,
257
+ pathJoin,
258
+ parseArgs,
259
+ nowIso,
260
+ loadTaskIndex,
261
+ loadRunning,
262
+ loadProjects,
263
+ loadJson,
264
+ getWorkHome,
265
+ getTaskIndexPath,
266
+ getRunningPath,
267
+ getProjectsPath,
268
+ getProjectDir,
269
+ getMonthCsvPath,
270
+ fmtElapsed,
271
+ expandHome,
272
+ ensureWorkHome,
273
+ ensureDir,
274
+ ensureCsvHeader,
275
+ appendCsvRow,
276
+ HEADER as CSV_HEADER
277
+ };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@vibecoded/work",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Work: CLI Time Tracker",
5
- "module": "dist/cli.js",
5
+ "main": "dist/lib.js",
6
+ "module": "dist/lib.js",
6
7
  "scripts": {
7
- "build": "bun build ./src/cli.ts --outdir=dist --target=node",
8
+ "build": "bun build ./src/cli.ts --outfile=dist/cli.js --target=node && bun build ./src/lib/index.ts --outfile=dist/lib.js --target=node",
8
9
  "prepublishOnly": "bun run build"
9
10
  },
10
11
  "devDependencies": {
@@ -21,6 +22,11 @@
21
22
  "bin": {
22
23
  "work": "dist/cli.js"
23
24
  },
25
+ "exports": {
26
+ ".": "./dist/lib.js",
27
+ "./cli": "./dist/cli.js",
28
+ "./lib": "./dist/lib.js"
29
+ },
24
30
  "files": [
25
31
  "dist",
26
32
  "README.md"