@usemeno/meno-cli 0.1.0 → 0.1.1

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/Asset 1.svg ADDED
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 55.44 45.4">
3
+ <g id="MenoLogo">
4
+ <path d="M43.78,28.72l-3.22-14.3-13.31,13.4c-2.15-1.65-3.92-3.4-5.43-5.75L42.15.78c.61-.64,3.06-1.02,3.67-.61s1.52,1.79,1.66,2.64l4.05,25.48-7.75.43Z"/>
5
+ <path d="M19.61,20.03l-6.25-5.12-2.48,13.76-8.11-.12L7.13,1.25c.22-1.13,4.18-1.52,5.05-.57l12.62,13.69-5.2,5.66Z"/>
6
+ <path d="M55.44,38.35c0,3.89-3.15,7.04-7.04,7.04-2.53,0-4.77-1.35-6.01-3.37H.36c-.4-1.61-.44-3.89-.23-6.03l41.85-.55c1.1-2.45,3.57-4.14,6.42-4.14,3.9,0,7.04,3.15,7.04,7.04Z"/>
7
+ </g>
8
+ </svg>
package/README.md CHANGED
@@ -132,7 +132,7 @@ Run `meno login` to authenticate.
132
132
  Run `meno select` to choose a project, or pass `--project <id>` to commands.
133
133
 
134
134
  **Connection errors:**
135
- Check your `baseUrl` in config. For production, use `https://app.meno.com`.
135
+ Check your `baseUrl` in config. For production, use `https://menohq.app`.
136
136
 
137
137
  ## Development
138
138
 
package/dist/index.js CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/index.ts
4
10
  import { Command } from "commander";
@@ -20,7 +26,7 @@ function setApiKey(key) {
20
26
  config.set("apiKey", key);
21
27
  }
22
28
  function getBaseUrl() {
23
- return config.get("baseUrl") || process.env.MENO_BASE_URL || "http://localhost:3000";
29
+ return config.get("baseUrl") || process.env.MENO_BASE_URL || "https://menohq.app";
24
30
  }
25
31
  function setBaseUrl(url) {
26
32
  config.set("baseUrl", url);
@@ -28,12 +34,6 @@ function setBaseUrl(url) {
28
34
  function getSelectedProject() {
29
35
  return config.get("selectedProject");
30
36
  }
31
- function setSelectedProject(project) {
32
- config.set("selectedProject", project);
33
- }
34
- function clearSelectedProject() {
35
- config.delete("selectedProject");
36
- }
37
37
  function getActiveTimer() {
38
38
  return config.get("activeTimer");
39
39
  }
@@ -43,6 +43,15 @@ function setActiveTimer(timer) {
43
43
  function clearActiveTimer() {
44
44
  config.delete("activeTimer");
45
45
  }
46
+ function getSelectedTask() {
47
+ return config.get("selectedTask");
48
+ }
49
+ function setSelectedTask(task) {
50
+ config.set("selectedTask", task);
51
+ }
52
+ function clearSelectedTask() {
53
+ config.delete("selectedTask");
54
+ }
46
55
 
47
56
  // src/utils/api.ts
48
57
  import fetch from "node-fetch";
@@ -109,6 +118,19 @@ async function apiRequest(endpoint, options = {}) {
109
118
  );
110
119
  }
111
120
  }
121
+ async function getTasks() {
122
+ const response = await apiRequest("/api/kanban/tasks");
123
+ return response.tasks || [];
124
+ }
125
+ async function cliAction(action, payload) {
126
+ return await apiRequest("/api/cli", {
127
+ method: "POST",
128
+ body: JSON.stringify({
129
+ action,
130
+ ...payload
131
+ })
132
+ });
133
+ }
112
134
 
113
135
  // src/commands/login.ts
114
136
  async function login() {
@@ -168,64 +190,90 @@ Unexpected error: ${error.message}
168
190
  import enquirer2 from "enquirer";
169
191
  import ora2 from "ora";
170
192
  import chalk2 from "chalk";
193
+ function getStatusColor(status) {
194
+ switch (status) {
195
+ case "Backlog":
196
+ return chalk2.gray;
197
+ case "Todo":
198
+ return chalk2.yellow;
199
+ case "InProgress":
200
+ return chalk2.cyan;
201
+ case "Review":
202
+ return chalk2.magenta;
203
+ case "Done":
204
+ return chalk2.green;
205
+ default:
206
+ return chalk2.white;
207
+ }
208
+ }
209
+ function formatTaskDisplay(task, isSelected) {
210
+ const prefix = isSelected ? "\u25CF " : " ";
211
+ const statusColor = getStatusColor(task.status);
212
+ const estimateText = task.estimatedHours ? ` \u2022 ${task.estimatedHours}h` : "";
213
+ return `${prefix}${chalk2.bold(task.title)} ${chalk2.dim("\u2192")} ${task.project.name} ${statusColor(`[${task.status}]`)}${estimateText}`;
214
+ }
171
215
  async function selectProject() {
172
- console.log(chalk2.bold("\n\u{1F4C2} Select Project\n"));
173
- const spinner = ora2("Loading projects...").start();
216
+ console.log(chalk2.bold("\n\u{1F4CB} Select Task\n"));
217
+ const spinner = ora2("Loading tasks...").start();
174
218
  try {
175
- const { projects } = await apiRequest("/api/external/projects");
219
+ const tasks = await getTasks();
176
220
  spinner.stop();
177
- if (projects.length === 0) {
178
- console.log(chalk2.yellow("No active projects found."));
179
- console.log(chalk2.dim("Create a project in the web app first.\n"));
221
+ if (tasks.length === 0) {
222
+ console.log(chalk2.yellow("No tasks found."));
223
+ console.log(chalk2.dim("Create tasks in the Meno dashboard first.\n"));
180
224
  return;
181
225
  }
182
- const currentProject = getSelectedProject();
226
+ const currentTask = getSelectedTask();
183
227
  const choices = [
184
228
  {
185
229
  name: "clear",
186
230
  message: chalk2.dim("[Clear Selection]")
187
231
  },
188
- ...projects.map((project) => {
189
- const isSelected = currentProject?.id === project.id;
190
- const prefix = isSelected ? "\u25CF " : " ";
191
- const displayName = `${project.name} (${project.clientName}) \u2022 $${project.hourlyRate}/hr`;
232
+ ...tasks.map((task) => {
233
+ const isSelected = currentTask?.id === task.id;
192
234
  return {
193
- name: project.id,
194
- message: prefix + displayName,
195
- value: project
235
+ name: task.id,
236
+ message: formatTaskDisplay(task, isSelected),
237
+ value: task
196
238
  };
197
239
  })
198
240
  ];
199
241
  const result = await enquirer2.prompt({
200
242
  type: "select",
201
- name: "project",
202
- message: "Select a project:",
243
+ name: "task",
244
+ message: "Select a task:",
203
245
  choices
204
246
  });
205
- const answer = result.project;
247
+ const answer = result.task;
206
248
  if (answer === "clear") {
207
- clearSelectedProject();
249
+ clearSelectedTask();
208
250
  console.log(chalk2.green("\n\u2713 Selection cleared\n"));
209
251
  } else {
210
- const project = projects.find((p) => p.id === answer);
211
- if (!project) {
212
- console.log(chalk2.red("\n\u2717 Project not found\n"));
252
+ const task = tasks.find((t) => t.id === answer);
253
+ if (!task) {
254
+ console.log(chalk2.red("\n\u2717 Task not found\n"));
213
255
  return;
214
256
  }
215
- setSelectedProject({
216
- id: project.id,
217
- name: project.name,
218
- clientName: project.clientName,
219
- hourlyRate: project.hourlyRate
257
+ setSelectedTask({
258
+ id: task.id,
259
+ title: task.title,
260
+ projectId: task.projectId,
261
+ projectName: task.project.name,
262
+ status: task.status,
263
+ estimatedHours: task.estimatedHours,
264
+ hourlyRate: task.project.hourlyRate
220
265
  });
266
+ const statusColor = getStatusColor(task.status);
221
267
  console.log(
222
268
  chalk2.green(`
223
- \u2713 Selected: ${chalk2.bold(project.name)} (${project.clientName}) \u2022 $${project.hourlyRate}/hr
224
- `)
269
+ \u2713 Selected: ${chalk2.bold(task.title)}
270
+ `) + chalk2.dim(` Project: ${task.project.name} \u2022 $${task.project.hourlyRate}/hr
271
+ `) + chalk2.dim(` Status: `) + statusColor(task.status) + (task.estimatedHours ? chalk2.dim(` \u2022 Estimated: ${task.estimatedHours}h
272
+ `) : "\n")
225
273
  );
226
274
  }
227
275
  } catch (error) {
228
- spinner.fail(chalk2.red("Failed to load projects"));
276
+ spinner.fail(chalk2.red("Failed to load tasks"));
229
277
  if (error instanceof ApiError) {
230
278
  console.log(chalk2.red(`
231
279
  Error: ${error.message}
@@ -276,35 +324,36 @@ function parseDuration(input) {
276
324
  }
277
325
 
278
326
  // src/commands/start.ts
279
- async function startTimer(projectId) {
327
+ async function startTimer(taskId) {
280
328
  const existingTimer = getActiveTimer();
281
329
  if (existingTimer) {
282
330
  const elapsed = formatDuration(existingTimer.startTime);
331
+ const taskInfo = existingTimer.taskTitle || existingTimer.projectName;
283
332
  console.log(
284
333
  chalk3.yellow(
285
334
  `
286
- \u26A0 Timer already running for ${chalk3.bold(existingTimer.projectName)} (${elapsed})`
335
+ \u26A0 Timer already running for ${chalk3.bold(taskInfo)} (${elapsed})`
287
336
  )
288
337
  );
289
338
  console.log(chalk3.dim(`Stop it first with ${chalk3.bold("meno stop")} or discard with ${chalk3.bold("meno stop --discard")}
290
339
  `));
291
340
  return;
292
341
  }
293
- let project;
294
- if (projectId) {
295
- const spinner = ora3("Loading project...").start();
342
+ let task;
343
+ if (taskId) {
344
+ const spinner2 = ora3("Loading task...").start();
296
345
  try {
297
- const { projects } = await apiRequest("/api/external/projects");
298
- project = projects.find((p) => p.id === projectId);
299
- spinner.stop();
300
- if (!project) {
346
+ const tasks = await getTasks();
347
+ task = tasks.find((t) => t.id === taskId);
348
+ spinner2.stop();
349
+ if (!task) {
301
350
  console.log(chalk3.red(`
302
- \u2717 Project not found: ${projectId}
351
+ \u2717 Task not found: ${taskId}
303
352
  `));
304
353
  return;
305
354
  }
306
355
  } catch (error) {
307
- spinner.fail(chalk3.red("Failed to load project"));
356
+ spinner2.fail(chalk3.red("Failed to load task"));
308
357
  if (error instanceof ApiError) {
309
358
  console.log(chalk3.red(`
310
359
  Error: ${error.message}
@@ -313,45 +362,122 @@ Error: ${error.message}
313
362
  return;
314
363
  }
315
364
  } else {
316
- const selected = getSelectedProject();
365
+ const selected = getSelectedTask();
317
366
  if (!selected) {
318
367
  console.log(chalk3.yellow(`
319
- \u26A0 No project selected.`));
320
- console.log(chalk3.dim(`Run ${chalk3.bold("meno select")} first or provide a project ID: ${chalk3.bold("meno start <project-id>")}
368
+ \u26A0 No task selected.`));
369
+ console.log(chalk3.dim(`Run ${chalk3.bold("meno select")} first or provide a task ID: ${chalk3.bold("meno start <task-id>")}
321
370
  `));
322
371
  return;
323
372
  }
324
- project = {
325
- id: selected.id,
326
- name: selected.name,
327
- clientName: selected.clientName,
328
- clientCompany: "",
329
- hourlyRate: selected.hourlyRate,
330
- taxRate: 0,
331
- weeklyHourLimit: 0,
332
- hoursUsed: 0
333
- };
373
+ const spinner2 = ora3("Starting timer...").start();
374
+ try {
375
+ const tasks = await getTasks();
376
+ task = tasks.find((t) => t.id === selected.id);
377
+ spinner2.stop();
378
+ if (!task) {
379
+ console.log(chalk3.red(`
380
+ \u2717 Selected task not found. It may have been deleted.
381
+ `));
382
+ return;
383
+ }
384
+ } catch (error) {
385
+ spinner2.fail(chalk3.red("Failed to load task"));
386
+ if (error instanceof ApiError) {
387
+ console.log(chalk3.red(`
388
+ Error: ${error.message}
389
+ `));
390
+ }
391
+ return;
392
+ }
393
+ }
394
+ const spinner = ora3("Starting timer...").start();
395
+ try {
396
+ const response = await cliAction("start", { taskId: task.id });
397
+ spinner.succeed(chalk3.green("Timer started"));
398
+ const startTime = response.timer?.startTime || (/* @__PURE__ */ new Date()).toISOString();
399
+ setActiveTimer({
400
+ projectId: task.projectId,
401
+ projectName: task.project.name,
402
+ startTime,
403
+ taskId: task.id,
404
+ taskTitle: task.title
405
+ });
406
+ const timestamp = new Date(startTime).toLocaleTimeString();
407
+ console.log(
408
+ chalk3.green(
409
+ `
410
+ \u23F1\uFE0F ${chalk3.bold(task.title)}
411
+ ` + chalk3.dim(` ${task.project.name} \u2022 $${task.project.hourlyRate}/hr \u2022 [${timestamp}]`)
412
+ )
413
+ );
414
+ if (task.estimatedHours) {
415
+ console.log(chalk3.dim(` Estimated: ${task.estimatedHours}h
416
+ `));
417
+ } else {
418
+ console.log();
419
+ }
420
+ } catch (error) {
421
+ spinner.fail(chalk3.red("Failed to start timer"));
422
+ if (error instanceof ApiError) {
423
+ console.log(chalk3.red(`
424
+ Error: ${error.message}
425
+ `));
426
+ } else {
427
+ console.log(chalk3.red(`
428
+ Unexpected error: ${error.message}
429
+ `));
430
+ }
334
431
  }
335
- const startTime = (/* @__PURE__ */ new Date()).toISOString();
336
- setActiveTimer({
337
- projectId: project.id,
338
- projectName: project.name,
339
- startTime
340
- });
341
- const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
342
- console.log(
343
- chalk3.green(
344
- `
345
- \u23F1\uFE0F Started: ${chalk3.bold(project.name)} \u2022 $${project.hourlyRate}/hr \u2022 [${timestamp}]
346
- `
347
- )
348
- );
349
432
  }
350
433
 
351
434
  // src/commands/stop.ts
352
435
  import enquirer3 from "enquirer";
353
436
  import chalk4 from "chalk";
354
437
  import ora4 from "ora";
438
+
439
+ // src/utils/git.ts
440
+ import { execSync } from "child_process";
441
+ function getGitInfo() {
442
+ try {
443
+ const commitHash = execSync("git rev-parse HEAD", {
444
+ encoding: "utf8",
445
+ stdio: ["pipe", "pipe", "ignore"]
446
+ // Suppress stderr
447
+ }).trim();
448
+ let repoUrl = execSync("git config --get remote.origin.url", {
449
+ encoding: "utf8",
450
+ stdio: ["pipe", "pipe", "ignore"]
451
+ }).trim();
452
+ if (repoUrl.startsWith("git@github.com:")) {
453
+ repoUrl = repoUrl.replace("git@github.com:", "https://github.com/").replace(/\.git$/, "");
454
+ } else if (repoUrl.endsWith(".git")) {
455
+ repoUrl = repoUrl.replace(/\.git$/, "");
456
+ }
457
+ if (!commitHash || !repoUrl) {
458
+ return null;
459
+ }
460
+ return {
461
+ commitHash,
462
+ repoUrl
463
+ };
464
+ } catch (error) {
465
+ return null;
466
+ }
467
+ }
468
+ function isGitRepository() {
469
+ try {
470
+ execSync("git rev-parse --is-inside-work-tree", {
471
+ encoding: "utf8",
472
+ stdio: ["pipe", "pipe", "ignore"]
473
+ });
474
+ return true;
475
+ } catch (error) {
476
+ return false;
477
+ }
478
+ }
479
+
480
+ // src/commands/stop.ts
355
481
  async function stopTimer(options) {
356
482
  const timer = getActiveTimer();
357
483
  if (!timer) {
@@ -368,41 +494,20 @@ async function stopTimer(options) {
368
494
  `));
369
495
  return;
370
496
  }
371
- let duration;
372
- let formatted;
373
- if (options.adjust) {
374
- duration = parseDuration(options.adjust);
375
- if (duration === 0) {
376
- console.log(chalk4.red(`
377
- \u2717 Invalid duration format: ${options.adjust}
378
- `));
379
- console.log(chalk4.dim(`Use format like: 1.5h, 90m, 45
380
- `));
381
- return;
382
- }
383
- const hours = Math.floor(duration);
384
- const minutes = Math.round((duration - hours) * 60);
385
- formatted = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
386
- console.log(chalk4.yellow(`
387
- \u26A0 Adjusted duration from timer
388
- `));
389
- } else {
390
- duration = calculateElapsedHours(timer.startTime);
391
- formatted = formatDuration(timer.startTime);
392
- }
393
- const project = getSelectedProject();
394
- const hourlyRate = project?.hourlyRate || 0;
395
- const amount = duration * hourlyRate;
497
+ const taskInfo = timer.taskTitle || timer.projectName;
498
+ const elapsed = formatDuration(timer.startTime);
396
499
  console.log(chalk4.bold(`
397
- \u23F1\uFE0F Duration: ${formatted} \u2022 $${amount.toFixed(2)}
500
+ \u23F1\uFE0F Stopping: ${taskInfo}`));
501
+ console.log(chalk4.dim(`Duration: ${elapsed}
398
502
  `));
399
503
  let description;
400
504
  let shouldLog = true;
505
+ let commitHash;
506
+ let repoUrl;
401
507
  try {
402
508
  if (options.description) {
403
509
  description = options.description;
404
- console.log(chalk4.dim(`Description: ${description}
405
- `));
510
+ console.log(chalk4.dim(`Description: ${description}`));
406
511
  } else {
407
512
  const descResult = await enquirer3.prompt({
408
513
  type: "input",
@@ -418,6 +523,34 @@ async function stopTimer(options) {
418
523
  });
419
524
  description = descResult.description;
420
525
  }
526
+ if (options.commit && options.repo) {
527
+ commitHash = options.commit;
528
+ repoUrl = options.repo;
529
+ console.log(chalk4.dim(`
530
+ \u{1F517} Evidence: ${commitHash.substring(0, 7)} @ ${repoUrl}`));
531
+ } else if (isGitRepository()) {
532
+ const gitInfo = getGitInfo();
533
+ if (gitInfo) {
534
+ const skipConfirm2 = options.noConfirm || options.yes;
535
+ if (skipConfirm2) {
536
+ commitHash = gitInfo.commitHash;
537
+ repoUrl = gitInfo.repoUrl;
538
+ console.log(chalk4.dim(`
539
+ \u{1F517} Evidence: ${commitHash.substring(0, 7)} @ ${repoUrl}`));
540
+ } else {
541
+ const gitResult = await enquirer3.prompt({
542
+ type: "confirm",
543
+ name: "includeGit",
544
+ message: `Include latest commit (${gitInfo.commitHash.substring(0, 7)}) as evidence?`,
545
+ initial: true
546
+ });
547
+ if (gitResult.includeGit) {
548
+ commitHash = gitInfo.commitHash;
549
+ repoUrl = gitInfo.repoUrl;
550
+ }
551
+ }
552
+ }
553
+ }
421
554
  const skipConfirm = options.noConfirm || options.yes;
422
555
  if (!skipConfirm) {
423
556
  const confirmResult = await enquirer3.prompt({
@@ -429,39 +562,44 @@ async function stopTimer(options) {
429
562
  shouldLog = confirmResult.shouldLog;
430
563
  if (!shouldLog) {
431
564
  console.log(chalk4.yellow("\n\u2717 Entry not logged\n"));
565
+ clearActiveTimer();
432
566
  return;
433
567
  }
434
568
  }
435
569
  const spinner = ora4("Logging entry...").start();
436
570
  try {
437
- const response = await apiRequest("/api/external/time-entries", {
438
- method: "POST",
439
- body: JSON.stringify({
440
- projectId: timer.projectId,
441
- description,
442
- duration: Math.round(duration * 100) / 100,
443
- // Round to 2 decimals
444
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
445
- // Today
446
- billable: true
447
- })
571
+ const response = await cliAction("stop", {
572
+ description,
573
+ commitHash,
574
+ repoUrl
448
575
  });
449
576
  clearActiveTimer();
450
- spinner.succeed(chalk4.green(`\u2713 Logged ${duration.toFixed(2)} hours to ${timer.projectName}`));
451
- console.log(chalk4.dim(`
452
- Entry created successfully
453
- `));
577
+ if (response.entry) {
578
+ const hours = response.entry.duration.toFixed(2);
579
+ const amount = response.entry.amount.toFixed(2);
580
+ spinner.succeed(chalk4.green(`\u2713 Logged ${hours} hours ($${amount})`));
581
+ if (response.evidence) {
582
+ console.log(chalk4.dim(`\u{1F4CE} Evidence: ${response.evidence.commitHash.substring(0, 7)}`));
583
+ }
584
+ console.log();
585
+ } else {
586
+ spinner.succeed(chalk4.green("\u2713 Timer stopped"));
587
+ console.log();
588
+ }
454
589
  } catch (error) {
455
- spinner.fail(chalk4.red("Failed to log entry"));
590
+ spinner.fail(chalk4.red("Failed to stop timer"));
456
591
  if (error instanceof ApiError) {
457
592
  console.log(chalk4.red(`
458
593
  Error: ${error.message}
594
+ `));
595
+ console.log(chalk4.yellow(`Timer cleared locally. You may need to check the dashboard.
459
596
  `));
460
597
  } else {
461
598
  console.log(chalk4.red(`
462
599
  Unexpected error: ${error.message}
463
600
  `));
464
601
  }
602
+ clearActiveTimer();
465
603
  }
466
604
  } catch (error) {
467
605
  console.log(chalk4.yellow("\n\u2717 Cancelled\n"));
@@ -564,27 +702,54 @@ Unexpected error: ${error.message}
564
702
  // src/commands/status.ts
565
703
  import chalk6 from "chalk";
566
704
  import ora6 from "ora";
705
+ function getStatusColor2(status) {
706
+ switch (status) {
707
+ case "Backlog":
708
+ return chalk6.gray;
709
+ case "Todo":
710
+ return chalk6.yellow;
711
+ case "InProgress":
712
+ return chalk6.cyan;
713
+ case "Review":
714
+ return chalk6.magenta;
715
+ case "Done":
716
+ return chalk6.green;
717
+ default:
718
+ return chalk6.white;
719
+ }
720
+ }
567
721
  async function showStatus() {
568
722
  console.log(chalk6.bold("\n\u{1F4CA} Meno Status\n"));
569
- const selected = getSelectedProject();
723
+ const selectedTask = getSelectedTask();
570
724
  const timer = getActiveTimer();
571
- if (selected) {
725
+ if (selectedTask) {
726
+ const statusColor = getStatusColor2(selectedTask.status);
572
727
  console.log(
573
728
  chalk6.cyan(
574
- `\u{1F4CC} Selected: ${chalk6.bold(selected.name)} (${selected.clientName}) \u2022 $${selected.hourlyRate}/hr`
729
+ `\u{1F4CC} Selected: ${chalk6.bold(selectedTask.title)}`
575
730
  )
576
731
  );
732
+ console.log(
733
+ chalk6.dim(
734
+ ` ${selectedTask.projectName} \u2022 $${selectedTask.hourlyRate}/hr \u2022 `
735
+ ) + statusColor(selectedTask.status)
736
+ );
737
+ if (selectedTask.estimatedHours) {
738
+ console.log(chalk6.dim(` Estimated: ${selectedTask.estimatedHours}h`));
739
+ }
577
740
  }
578
741
  if (timer) {
579
742
  const elapsed = formatDuration(timer.startTime);
580
- const value = calculateTimerValue(timer.startTime, selected?.hourlyRate || 0);
743
+ const taskInfo = timer.taskTitle || timer.projectName;
744
+ const rate = selectedTask?.hourlyRate || 0;
745
+ const value = calculateTimerValue(timer.startTime, rate);
581
746
  console.log(
582
747
  chalk6.green(
583
- `\u23F1\uFE0F Running: ${chalk6.bold(timer.projectName)} \u2022 ${elapsed} \u2022 $${value.toFixed(2)}`
748
+ `\u23F1\uFE0F Running: ${chalk6.bold(taskInfo)} \u2022 ${elapsed} \u2022 $${value.toFixed(2)}`
584
749
  )
585
750
  );
586
751
  }
587
- if (selected || timer) {
752
+ if (selectedTask || timer) {
588
753
  console.log();
589
754
  }
590
755
  const spinner = ora6("Loading stats...").start();
@@ -614,13 +779,69 @@ Unexpected error: ${error.message}
614
779
  }
615
780
  }
616
781
 
782
+ // src/logo-ascii.ts
783
+ var SVG_EMBED = `<?xml version="1.0" encoding="UTF-8"?>
784
+ <svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 55.44 45.4">
785
+ <g id="MenoLogo">
786
+ <path d="M43.78,28.72l-3.22-14.3-13.31,13.4c-2.15-1.65-3.92-3.4-5.43-5.75L42.15.78c.61-.64,3.06-1.02,3.67-.61s1.52,1.79,1.66,2.64l4.05,25.48-7.75.43Z"/>
787
+ <path d="M19.61,20.03l-6.25-5.12-2.48,13.76-8.11-.12L7.13,1.25c.22-1.13,4.18-1.52,5.05-.57l12.62,13.69-5.2,5.66Z"/>
788
+ <path d="M55.44,38.35c0,3.89-3.15,7.04-7.04,7.04-2.53,0-4.77-1.35-6.01-3.37H.36c-.4-1.61-.44-3.89-.23-6.03l41.85-.55c1.1-2.45,3.57-4.14,6.42-4.14,3.9,0,7.04,3.15,7.04,7.04Z"/>
789
+ </g>
790
+ </svg>`;
791
+ function fallbackAscii() {
792
+ return [
793
+ " ____ __ ",
794
+ " /\\ / __ \\/ /___ _ _ ",
795
+ " / \\ _ __ / / / / / __ \\ | | |",
796
+ " / /\\ \\ | '_ \\ / /_/ / / /_/ / |_| |",
797
+ " / ____ \\| | | |\\____/_/\\____/\\__,_|",
798
+ " /_/ \\_\\_| |_| ",
799
+ "",
800
+ " M E N O (logo) "
801
+ ].join("\n");
802
+ }
803
+ async function printLogoAscii(width = 64) {
804
+ try {
805
+ const sharpImport = await import("sharp");
806
+ const sharp = sharpImport.default ?? sharpImport;
807
+ const svgBuffer = Buffer.from(SVG_EMBED, "utf8");
808
+ const img = sharp(svgBuffer).resize({ width }).flatten({ background: { r: 255, g: 255, b: 255 } }).grayscale();
809
+ const { data, info } = await img.raw().toBuffer({ resolveWithObject: true });
810
+ const chars = " .,:;i1tfLCG08@";
811
+ const rows = [];
812
+ const aspect = 0.5;
813
+ const rowStep = Math.max(1, Math.round(1 / aspect));
814
+ for (let y = 0; y < info.height; y += rowStep) {
815
+ let line = "";
816
+ for (let x = 0; x < info.width; x++) {
817
+ const idx = y * info.width + x;
818
+ const val = data[idx];
819
+ const charIdx = Math.floor((1 - val / 255) * (chars.length - 1));
820
+ line += chars[charIdx];
821
+ }
822
+ rows.push(line);
823
+ }
824
+ console.log(rows.join("\n"));
825
+ return;
826
+ } catch (err) {
827
+ console.log(fallbackAscii());
828
+ return;
829
+ }
830
+ }
831
+ if (__require.main === module) {
832
+ printLogoAscii().catch(() => {
833
+ });
834
+ }
835
+
617
836
  // src/index.ts
618
837
  var program = new Command();
619
838
  program.name("meno").description("CLI for Meno time tracking").version("0.1.0");
839
+ printLogoAscii().catch(() => {
840
+ });
620
841
  program.command("login").description("Authenticate with your Meno API key").action(login);
621
- program.command("select").description("Select a project to work on").action(selectProject);
622
- program.command("start [project-id]").description("Start a timer on selected project or specific project ID").action(startTimer);
623
- program.command("stop").description("Stop the running timer and log the entry").option("--discard", "Discard the timer without logging").option("-d, --description <text>", "Entry description (skip prompt)").option("-y, --yes", "Auto-confirm (skip confirmation prompt)").option("--no-confirm", "Skip confirmation prompt").option("--adjust <duration>", "Override calculated duration (e.g., 1.5h, 45m)").action(stopTimer);
842
+ program.command("select").description("Select a task to work on").action(selectProject);
843
+ program.command("start [task-id]").description("Start a timer on selected task or specific task ID").action(startTimer);
844
+ program.command("stop").description("Stop the running timer and log the entry").option("--discard", "Discard the timer without logging").option("-d, --description <text>", "Entry description (skip prompt)").option("-y, --yes", "Auto-confirm (skip confirmation prompt)").option("--no-confirm", "Skip confirmation prompt").option("--commit <hash>", "Git commit hash for evidence").option("--repo <url>", "Git repository URL for evidence").action(stopTimer);
624
845
  program.command("log <description>").description("Log time manually").option("-d, --duration <duration>", "Duration (e.g., 45m, 1.5h, 90)").option("-p, --project <id>", "Project ID").action(logTime);
625
- program.command("status").description("Show selected project, timer status, and unbilled stats").action(showStatus);
846
+ program.command("status").description("Show selected task, timer status, and unbilled stats").action(showStatus);
626
847
  program.parse();