UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

98 lines (97 loc) • 4.29 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * Shared time and date utilities. * * Centralises parsing of user-facing `--since` values so that all consumers * interpret the same set of formats (ISO-8601, epoch ms, plain date strings) * consistently without private re-implementations drifting apart. */ import { UsageError } from "./errors.js"; // ── Duration-shorthand parsing ─────────────────────────────────────────────── const MINUTE_MS = 60 * 1000; const HOUR_MS = 60 * 60 * 1000; const DAY_MS = 24 * 60 * 60 * 1000; /** A month is approximated as 30 days — this shorthand is not calendar-exact. */ const MONTH_MS = 30 * DAY_MS; /** * Canonical duration-shorthand unit map shared by every `--since` / `--expires` * / `--window-compare` consumer. * * The grammar is intentionally uniform across the whole CLI: * - `m` = MINUTES, `M` = MONTHS (30-day approximation) * - `h` = hours, `d` = days * * Matching is CASE-SENSITIVE (see {@link parseDuration}), which is what lets * `m` and `M` mean different things. Historically `akm health --since` and * `remember --expires` read a case-insensitive `m` as MONTHS while * `consolidate` / `--window-compare` read it as MINUTES; that split is now * resolved in favour of the conventional `m`=minutes, with `M` reserved for * months. */ export const DURATION_UNITS = { m: MINUTE_MS, M: MONTH_MS, h: HOUR_MS, d: DAY_MS, }; /** * Parse a compact duration shorthand (e.g. `"30d"`, `"12h"`, `"5m"`, `"3M"`) * into a number of milliseconds using an explicit `units` map (default * {@link DURATION_UNITS}), or return `null` when the input does not match * `<digits><letter>` or the unit is not in the map. * * Matching is CASE-SENSITIVE against the map keys, so `m` (minutes) and `M` * (months) are distinct — do NOT lower-case the spec before calling, or the * two collapse. Amount is parsed with base-10 `parseInt`; `null` is returned * rather than throwing so each caller keeps its own error/fallback policy. */ export function parseDuration(spec, units = DURATION_UNITS) { const match = spec.trim().match(/^(\d+)([a-zA-Z])$/); if (!match) return null; const amount = Number.parseInt(match[1] ?? "", 10); if (!Number.isFinite(amount)) return null; const multiplier = units[match[2] ?? ""]; if (multiplier === undefined) return null; return amount * multiplier; } // ── Since-flag parsing ─────────────────────────────────────────────────────── /** * Parse a user-supplied `--since` value and return an ISO-8601 timestamp * string (e.g. `"2026-01-15T10:30:00.000Z"`). * * Accepted input formats: * - ISO-8601 timestamp (preferred): `"2026-04-01T00:00:00Z"` * - Plain date: `"2026-04-01"` (interpreted as start-of-day UTC) * - Epoch milliseconds (pure digit string): `"1744329600000"` * - Any other value parseable by `new Date()` * * Callers that need a different wire format (e.g. SQLite `"YYYY-MM-DD HH:MM:SS"`) * should convert the returned ISO string themselves. * * @throws {UsageError} when `since` is empty or cannot be parsed as a date. */ export function parseSinceToIso(since) { const trimmed = since.trim(); if (!trimmed) { throw new UsageError("--since cannot be empty.", "INVALID_FLAG_VALUE"); } // Pure-digit input → epoch milliseconds if (/^\d+$/.test(trimmed)) { const ms = Number.parseInt(trimmed, 10); const d = new Date(ms); if (Number.isNaN(d.getTime())) { throw new UsageError(`Invalid --since value: ${since}`, "INVALID_FLAG_VALUE"); } return d.toISOString(); } const parsed = new Date(trimmed); if (Number.isNaN(parsed.getTime())) { throw new UsageError(`Invalid --since value: ${since}. Expected ISO timestamp (e.g. 2026-04-01T00:00:00Z) or epoch ms.`, "INVALID_FLAG_VALUE"); } return parsed.toISOString(); }