@usemeno/meno-cli 0.1.0

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.
@@ -0,0 +1,113 @@
1
+ import fetch from "node-fetch";
2
+ import { getApiKey, getBaseUrl } from "../config.js";
3
+
4
+ export class ApiError extends Error {
5
+ constructor(
6
+ message: string,
7
+ public statusCode: number
8
+ ) {
9
+ super(message);
10
+ this.name = "ApiError";
11
+ }
12
+ }
13
+
14
+ export async function apiRequest<T = any>(
15
+ endpoint: string,
16
+ options: RequestInit & { body?: string } = {}
17
+ ): Promise<T> {
18
+ const apiKey = getApiKey();
19
+ const baseUrl = getBaseUrl();
20
+
21
+ if (!apiKey) {
22
+ throw new ApiError(
23
+ "Not logged in. Run 'meno login' to authenticate.",
24
+ 401
25
+ );
26
+ }
27
+
28
+ const url = `${baseUrl}${endpoint}`;
29
+
30
+ const headers = {
31
+ "Authorization": `Bearer ${apiKey}`,
32
+ "Content-Type": "application/json",
33
+ ...options.headers,
34
+ };
35
+
36
+ try {
37
+ const response = await fetch(url, {
38
+ ...options,
39
+ headers,
40
+ });
41
+
42
+ const data = await response.json() as any;
43
+
44
+ if (!response.ok) {
45
+ if (response.status === 401) {
46
+ throw new ApiError(
47
+ "Invalid API key. Run 'meno login' to re-authenticate.",
48
+ 401
49
+ );
50
+ }
51
+
52
+ if (response.status === 429) {
53
+ throw new ApiError(
54
+ `Rate limit exceeded. ${data.error || "Try again later."}`,
55
+ 429
56
+ );
57
+ }
58
+
59
+ throw new ApiError(
60
+ data.error || `Request failed with status ${response.status}`,
61
+ response.status
62
+ );
63
+ }
64
+
65
+ return data as T;
66
+ } catch (error: any) {
67
+ if (error instanceof ApiError) {
68
+ throw error;
69
+ }
70
+
71
+ if (error.code === "ENOTFOUND" || error.code === "ECONNREFUSED") {
72
+ throw new ApiError(
73
+ `Cannot connect to ${baseUrl}. Check your internet connection or base URL.`,
74
+ 0
75
+ );
76
+ }
77
+
78
+ throw new ApiError(
79
+ error.message || "An unexpected error occurred",
80
+ 500
81
+ );
82
+ }
83
+ }
84
+
85
+ export interface Project {
86
+ id: string;
87
+ name: string;
88
+ clientName: string;
89
+ clientCompany: string;
90
+ hourlyRate: number;
91
+ taxRate: number;
92
+ weeklyHourLimit: number;
93
+ hoursUsed: number;
94
+ }
95
+
96
+ export interface TimeEntry {
97
+ id: string;
98
+ projectId: string;
99
+ projectName: string;
100
+ description: string;
101
+ date: string;
102
+ duration: number;
103
+ billable: boolean;
104
+ amount: number;
105
+ createdAt: string;
106
+ }
107
+
108
+ export interface Stats {
109
+ totalHours: number;
110
+ totalRevenue: number;
111
+ entriesCount: number;
112
+ currency: string;
113
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Formats milliseconds into human-readable duration
3
+ * @param startTime ISO timestamp of when timer started
4
+ * @returns Formatted duration string like "2 hours 30 minutes" or "45 minutes"
5
+ */
6
+ export function formatDuration(startTime: string): string {
7
+ const elapsed = Date.now() - new Date(startTime).getTime();
8
+ const hours = Math.floor(elapsed / (1000 * 60 * 60));
9
+ const minutes = Math.floor((elapsed % (1000 * 60 * 60)) / (1000 * 60));
10
+
11
+ if (hours > 0) {
12
+ return `${hours} ${hours === 1 ? "hour" : "hours"} ${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
13
+ }
14
+
15
+ return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
16
+ }
17
+
18
+ /**
19
+ * Calculates elapsed hours from start time
20
+ * @param startTime ISO timestamp
21
+ * @returns Hours as decimal (e.g., 1.5 for 90 minutes)
22
+ */
23
+ export function calculateElapsedHours(startTime: string): number {
24
+ const elapsed = Date.now() - new Date(startTime).getTime();
25
+ return elapsed / (1000 * 60 * 60);
26
+ }
27
+
28
+ /**
29
+ * Calculates current billable value of running timer
30
+ * @param startTime ISO timestamp
31
+ * @param hourlyRate Rate per hour
32
+ * @returns Current billable amount
33
+ */
34
+ export function calculateTimerValue(startTime: string, hourlyRate: number): number {
35
+ const hours = calculateElapsedHours(startTime);
36
+ return hours * hourlyRate;
37
+ }
38
+
39
+ /**
40
+ * Parses duration input into decimal hours
41
+ * Supports formats: "45m", "1.5h", "90" (assumes minutes)
42
+ * @param input Duration string
43
+ * @returns Hours as decimal, or null if invalid
44
+ */
45
+ export function parseDuration(input: string): number | null {
46
+ const trimmed = input.trim().toLowerCase();
47
+
48
+ // Match patterns: "45m", "1.5h", "90"
49
+ const minutesMatch = trimmed.match(/^(\d+(?:\.\d+)?)m?$/);
50
+ const hoursMatch = trimmed.match(/^(\d+(?:\.\d+)?)h$/);
51
+
52
+ if (hoursMatch) {
53
+ return parseFloat(hoursMatch[1]);
54
+ }
55
+
56
+ if (minutesMatch) {
57
+ const minutes = parseFloat(minutesMatch[1]);
58
+ return minutes / 60;
59
+ }
60
+
61
+ return null;
62
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022"],
6
+ "moduleResolution": "bundler",
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "strict": true,
10
+ "resolveJsonModule": true,
11
+ "outDir": "./dist",
12
+ "rootDir": "./src",
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "types": ["node"]
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }