@usemeno/meno-cli 0.1.0 → 0.1.2

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
@@ -20,7 +20,7 @@ function setApiKey(key) {
20
20
  config.set("apiKey", key);
21
21
  }
22
22
  function getBaseUrl() {
23
- return config.get("baseUrl") || process.env.MENO_BASE_URL || "http://localhost:3000";
23
+ return config.get("baseUrl") || process.env.MENO_BASE_URL || "https://menohq.app";
24
24
  }
25
25
  function setBaseUrl(url) {
26
26
  config.set("baseUrl", url);
@@ -28,12 +28,6 @@ function setBaseUrl(url) {
28
28
  function getSelectedProject() {
29
29
  return config.get("selectedProject");
30
30
  }
31
- function setSelectedProject(project) {
32
- config.set("selectedProject", project);
33
- }
34
- function clearSelectedProject() {
35
- config.delete("selectedProject");
36
- }
37
31
  function getActiveTimer() {
38
32
  return config.get("activeTimer");
39
33
  }
@@ -43,6 +37,15 @@ function setActiveTimer(timer) {
43
37
  function clearActiveTimer() {
44
38
  config.delete("activeTimer");
45
39
  }
40
+ function getSelectedTask() {
41
+ return config.get("selectedTask");
42
+ }
43
+ function setSelectedTask(task) {
44
+ config.set("selectedTask", task);
45
+ }
46
+ function clearSelectedTask() {
47
+ config.delete("selectedTask");
48
+ }
46
49
 
47
50
  // src/utils/api.ts
48
51
  import fetch from "node-fetch";
@@ -109,6 +112,19 @@ async function apiRequest(endpoint, options = {}) {
109
112
  );
110
113
  }
111
114
  }
115
+ async function getTasks() {
116
+ const response = await apiRequest("/api/kanban/tasks");
117
+ return response.tasks || [];
118
+ }
119
+ async function cliAction(action, payload) {
120
+ return await apiRequest("/api/cli", {
121
+ method: "POST",
122
+ body: JSON.stringify({
123
+ action,
124
+ ...payload
125
+ })
126
+ });
127
+ }
112
128
 
113
129
  // src/commands/login.ts
114
130
  async function login() {
@@ -168,64 +184,90 @@ Unexpected error: ${error.message}
168
184
  import enquirer2 from "enquirer";
169
185
  import ora2 from "ora";
170
186
  import chalk2 from "chalk";
187
+ function getStatusColor(status) {
188
+ switch (status) {
189
+ case "Backlog":
190
+ return chalk2.gray;
191
+ case "Todo":
192
+ return chalk2.yellow;
193
+ case "InProgress":
194
+ return chalk2.cyan;
195
+ case "Review":
196
+ return chalk2.magenta;
197
+ case "Done":
198
+ return chalk2.green;
199
+ default:
200
+ return chalk2.white;
201
+ }
202
+ }
203
+ function formatTaskDisplay(task, isSelected) {
204
+ const prefix = isSelected ? "\u25CF " : " ";
205
+ const statusColor = getStatusColor(task.status);
206
+ const estimateText = task.estimatedHours ? ` \u2022 ${task.estimatedHours}h` : "";
207
+ return `${prefix}${chalk2.bold(task.title)} ${chalk2.dim("\u2192")} ${task.project.name} ${statusColor(`[${task.status}]`)}${estimateText}`;
208
+ }
171
209
  async function selectProject() {
172
- console.log(chalk2.bold("\n\u{1F4C2} Select Project\n"));
173
- const spinner = ora2("Loading projects...").start();
210
+ console.log(chalk2.bold("\n\u{1F4CB} Select Task\n"));
211
+ const spinner = ora2("Loading tasks...").start();
174
212
  try {
175
- const { projects } = await apiRequest("/api/external/projects");
213
+ const tasks = await getTasks();
176
214
  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"));
215
+ if (tasks.length === 0) {
216
+ console.log(chalk2.yellow("No tasks found."));
217
+ console.log(chalk2.dim("Create tasks in the Meno dashboard first.\n"));
180
218
  return;
181
219
  }
182
- const currentProject = getSelectedProject();
220
+ const currentTask = getSelectedTask();
183
221
  const choices = [
184
222
  {
185
223
  name: "clear",
186
224
  message: chalk2.dim("[Clear Selection]")
187
225
  },
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`;
226
+ ...tasks.map((task) => {
227
+ const isSelected = currentTask?.id === task.id;
192
228
  return {
193
- name: project.id,
194
- message: prefix + displayName,
195
- value: project
229
+ name: task.id,
230
+ message: formatTaskDisplay(task, isSelected),
231
+ value: task
196
232
  };
197
233
  })
198
234
  ];
199
235
  const result = await enquirer2.prompt({
200
236
  type: "select",
201
- name: "project",
202
- message: "Select a project:",
237
+ name: "task",
238
+ message: "Select a task:",
203
239
  choices
204
240
  });
205
- const answer = result.project;
241
+ const answer = result.task;
206
242
  if (answer === "clear") {
207
- clearSelectedProject();
243
+ clearSelectedTask();
208
244
  console.log(chalk2.green("\n\u2713 Selection cleared\n"));
209
245
  } else {
210
- const project = projects.find((p) => p.id === answer);
211
- if (!project) {
212
- console.log(chalk2.red("\n\u2717 Project not found\n"));
246
+ const task = tasks.find((t) => t.id === answer);
247
+ if (!task) {
248
+ console.log(chalk2.red("\n\u2717 Task not found\n"));
213
249
  return;
214
250
  }
215
- setSelectedProject({
216
- id: project.id,
217
- name: project.name,
218
- clientName: project.clientName,
219
- hourlyRate: project.hourlyRate
251
+ setSelectedTask({
252
+ id: task.id,
253
+ title: task.title,
254
+ projectId: task.projectId,
255
+ projectName: task.project.name,
256
+ status: task.status,
257
+ estimatedHours: task.estimatedHours,
258
+ hourlyRate: task.project.hourlyRate
220
259
  });
260
+ const statusColor = getStatusColor(task.status);
221
261
  console.log(
222
262
  chalk2.green(`
223
- \u2713 Selected: ${chalk2.bold(project.name)} (${project.clientName}) \u2022 $${project.hourlyRate}/hr
224
- `)
263
+ \u2713 Selected: ${chalk2.bold(task.title)}
264
+ `) + chalk2.dim(` Project: ${task.project.name} \u2022 $${task.project.hourlyRate}/hr
265
+ `) + chalk2.dim(` Status: `) + statusColor(task.status) + (task.estimatedHours ? chalk2.dim(` \u2022 Estimated: ${task.estimatedHours}h
266
+ `) : "\n")
225
267
  );
226
268
  }
227
269
  } catch (error) {
228
- spinner.fail(chalk2.red("Failed to load projects"));
270
+ spinner.fail(chalk2.red("Failed to load tasks"));
229
271
  if (error instanceof ApiError) {
230
272
  console.log(chalk2.red(`
231
273
  Error: ${error.message}
@@ -276,35 +318,36 @@ function parseDuration(input) {
276
318
  }
277
319
 
278
320
  // src/commands/start.ts
279
- async function startTimer(projectId) {
321
+ async function startTimer(taskId) {
280
322
  const existingTimer = getActiveTimer();
281
323
  if (existingTimer) {
282
324
  const elapsed = formatDuration(existingTimer.startTime);
325
+ const taskInfo = existingTimer.taskTitle || existingTimer.projectName;
283
326
  console.log(
284
327
  chalk3.yellow(
285
328
  `
286
- \u26A0 Timer already running for ${chalk3.bold(existingTimer.projectName)} (${elapsed})`
329
+ \u26A0 Timer already running for ${chalk3.bold(taskInfo)} (${elapsed})`
287
330
  )
288
331
  );
289
332
  console.log(chalk3.dim(`Stop it first with ${chalk3.bold("meno stop")} or discard with ${chalk3.bold("meno stop --discard")}
290
333
  `));
291
334
  return;
292
335
  }
293
- let project;
294
- if (projectId) {
295
- const spinner = ora3("Loading project...").start();
336
+ let task;
337
+ if (taskId) {
338
+ const spinner2 = ora3("Loading task...").start();
296
339
  try {
297
- const { projects } = await apiRequest("/api/external/projects");
298
- project = projects.find((p) => p.id === projectId);
299
- spinner.stop();
300
- if (!project) {
340
+ const tasks = await getTasks();
341
+ task = tasks.find((t) => t.id === taskId);
342
+ spinner2.stop();
343
+ if (!task) {
301
344
  console.log(chalk3.red(`
302
- \u2717 Project not found: ${projectId}
345
+ \u2717 Task not found: ${taskId}
303
346
  `));
304
347
  return;
305
348
  }
306
349
  } catch (error) {
307
- spinner.fail(chalk3.red("Failed to load project"));
350
+ spinner2.fail(chalk3.red("Failed to load task"));
308
351
  if (error instanceof ApiError) {
309
352
  console.log(chalk3.red(`
310
353
  Error: ${error.message}
@@ -313,45 +356,122 @@ Error: ${error.message}
313
356
  return;
314
357
  }
315
358
  } else {
316
- const selected = getSelectedProject();
359
+ const selected = getSelectedTask();
317
360
  if (!selected) {
318
361
  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>")}
362
+ \u26A0 No task selected.`));
363
+ console.log(chalk3.dim(`Run ${chalk3.bold("meno select")} first or provide a task ID: ${chalk3.bold("meno start <task-id>")}
321
364
  `));
322
365
  return;
323
366
  }
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
- };
367
+ const spinner2 = ora3("Starting timer...").start();
368
+ try {
369
+ const tasks = await getTasks();
370
+ task = tasks.find((t) => t.id === selected.id);
371
+ spinner2.stop();
372
+ if (!task) {
373
+ console.log(chalk3.red(`
374
+ \u2717 Selected task not found. It may have been deleted.
375
+ `));
376
+ return;
377
+ }
378
+ } catch (error) {
379
+ spinner2.fail(chalk3.red("Failed to load task"));
380
+ if (error instanceof ApiError) {
381
+ console.log(chalk3.red(`
382
+ Error: ${error.message}
383
+ `));
384
+ }
385
+ return;
386
+ }
387
+ }
388
+ const spinner = ora3("Starting timer...").start();
389
+ try {
390
+ const response = await cliAction("start", { taskId: task.id });
391
+ spinner.succeed(chalk3.green("Timer started"));
392
+ const startTime = response.timer?.startTime || (/* @__PURE__ */ new Date()).toISOString();
393
+ setActiveTimer({
394
+ projectId: task.projectId,
395
+ projectName: task.project.name,
396
+ startTime,
397
+ taskId: task.id,
398
+ taskTitle: task.title
399
+ });
400
+ const timestamp = new Date(startTime).toLocaleTimeString();
401
+ console.log(
402
+ chalk3.green(
403
+ `
404
+ \u23F1\uFE0F ${chalk3.bold(task.title)}
405
+ ` + chalk3.dim(` ${task.project.name} \u2022 $${task.project.hourlyRate}/hr \u2022 [${timestamp}]`)
406
+ )
407
+ );
408
+ if (task.estimatedHours) {
409
+ console.log(chalk3.dim(` Estimated: ${task.estimatedHours}h
410
+ `));
411
+ } else {
412
+ console.log();
413
+ }
414
+ } catch (error) {
415
+ spinner.fail(chalk3.red("Failed to start timer"));
416
+ if (error instanceof ApiError) {
417
+ console.log(chalk3.red(`
418
+ Error: ${error.message}
419
+ `));
420
+ } else {
421
+ console.log(chalk3.red(`
422
+ Unexpected error: ${error.message}
423
+ `));
424
+ }
334
425
  }
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
426
  }
350
427
 
351
428
  // src/commands/stop.ts
352
429
  import enquirer3 from "enquirer";
353
430
  import chalk4 from "chalk";
354
431
  import ora4 from "ora";
432
+
433
+ // src/utils/git.ts
434
+ import { execSync } from "child_process";
435
+ function getGitInfo() {
436
+ try {
437
+ const commitHash = execSync("git rev-parse HEAD", {
438
+ encoding: "utf8",
439
+ stdio: ["pipe", "pipe", "ignore"]
440
+ // Suppress stderr
441
+ }).trim();
442
+ let repoUrl = execSync("git config --get remote.origin.url", {
443
+ encoding: "utf8",
444
+ stdio: ["pipe", "pipe", "ignore"]
445
+ }).trim();
446
+ if (repoUrl.startsWith("git@github.com:")) {
447
+ repoUrl = repoUrl.replace("git@github.com:", "https://github.com/").replace(/\.git$/, "");
448
+ } else if (repoUrl.endsWith(".git")) {
449
+ repoUrl = repoUrl.replace(/\.git$/, "");
450
+ }
451
+ if (!commitHash || !repoUrl) {
452
+ return null;
453
+ }
454
+ return {
455
+ commitHash,
456
+ repoUrl
457
+ };
458
+ } catch (error) {
459
+ return null;
460
+ }
461
+ }
462
+ function isGitRepository() {
463
+ try {
464
+ execSync("git rev-parse --is-inside-work-tree", {
465
+ encoding: "utf8",
466
+ stdio: ["pipe", "pipe", "ignore"]
467
+ });
468
+ return true;
469
+ } catch (error) {
470
+ return false;
471
+ }
472
+ }
473
+
474
+ // src/commands/stop.ts
355
475
  async function stopTimer(options) {
356
476
  const timer = getActiveTimer();
357
477
  if (!timer) {
@@ -368,41 +488,20 @@ async function stopTimer(options) {
368
488
  `));
369
489
  return;
370
490
  }
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;
491
+ const taskInfo = timer.taskTitle || timer.projectName;
492
+ const elapsed = formatDuration(timer.startTime);
396
493
  console.log(chalk4.bold(`
397
- \u23F1\uFE0F Duration: ${formatted} \u2022 $${amount.toFixed(2)}
494
+ \u23F1\uFE0F Stopping: ${taskInfo}`));
495
+ console.log(chalk4.dim(`Duration: ${elapsed}
398
496
  `));
399
497
  let description;
400
498
  let shouldLog = true;
499
+ let commitHash;
500
+ let repoUrl;
401
501
  try {
402
502
  if (options.description) {
403
503
  description = options.description;
404
- console.log(chalk4.dim(`Description: ${description}
405
- `));
504
+ console.log(chalk4.dim(`Description: ${description}`));
406
505
  } else {
407
506
  const descResult = await enquirer3.prompt({
408
507
  type: "input",
@@ -418,6 +517,34 @@ async function stopTimer(options) {
418
517
  });
419
518
  description = descResult.description;
420
519
  }
520
+ if (options.commit && options.repo) {
521
+ commitHash = options.commit;
522
+ repoUrl = options.repo;
523
+ console.log(chalk4.dim(`
524
+ \u{1F517} Evidence: ${commitHash.substring(0, 7)} @ ${repoUrl}`));
525
+ } else if (isGitRepository()) {
526
+ const gitInfo = getGitInfo();
527
+ if (gitInfo) {
528
+ const skipConfirm2 = options.noConfirm || options.yes;
529
+ if (skipConfirm2) {
530
+ commitHash = gitInfo.commitHash;
531
+ repoUrl = gitInfo.repoUrl;
532
+ console.log(chalk4.dim(`
533
+ \u{1F517} Evidence: ${commitHash.substring(0, 7)} @ ${repoUrl}`));
534
+ } else {
535
+ const gitResult = await enquirer3.prompt({
536
+ type: "confirm",
537
+ name: "includeGit",
538
+ message: `Include latest commit (${gitInfo.commitHash.substring(0, 7)}) as evidence?`,
539
+ initial: true
540
+ });
541
+ if (gitResult.includeGit) {
542
+ commitHash = gitInfo.commitHash;
543
+ repoUrl = gitInfo.repoUrl;
544
+ }
545
+ }
546
+ }
547
+ }
421
548
  const skipConfirm = options.noConfirm || options.yes;
422
549
  if (!skipConfirm) {
423
550
  const confirmResult = await enquirer3.prompt({
@@ -429,39 +556,44 @@ async function stopTimer(options) {
429
556
  shouldLog = confirmResult.shouldLog;
430
557
  if (!shouldLog) {
431
558
  console.log(chalk4.yellow("\n\u2717 Entry not logged\n"));
559
+ clearActiveTimer();
432
560
  return;
433
561
  }
434
562
  }
435
563
  const spinner = ora4("Logging entry...").start();
436
564
  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
- })
565
+ const response = await cliAction("stop", {
566
+ description,
567
+ commitHash,
568
+ repoUrl
448
569
  });
449
570
  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
- `));
571
+ if (response.entry) {
572
+ const hours = response.entry.duration.toFixed(2);
573
+ const amount = response.entry.amount.toFixed(2);
574
+ spinner.succeed(chalk4.green(`\u2713 Logged ${hours} hours ($${amount})`));
575
+ if (response.evidence) {
576
+ console.log(chalk4.dim(`\u{1F4CE} Evidence: ${response.evidence.commitHash.substring(0, 7)}`));
577
+ }
578
+ console.log();
579
+ } else {
580
+ spinner.succeed(chalk4.green("\u2713 Timer stopped"));
581
+ console.log();
582
+ }
454
583
  } catch (error) {
455
- spinner.fail(chalk4.red("Failed to log entry"));
584
+ spinner.fail(chalk4.red("Failed to stop timer"));
456
585
  if (error instanceof ApiError) {
457
586
  console.log(chalk4.red(`
458
587
  Error: ${error.message}
588
+ `));
589
+ console.log(chalk4.yellow(`Timer cleared locally. You may need to check the dashboard.
459
590
  `));
460
591
  } else {
461
592
  console.log(chalk4.red(`
462
593
  Unexpected error: ${error.message}
463
594
  `));
464
595
  }
596
+ clearActiveTimer();
465
597
  }
466
598
  } catch (error) {
467
599
  console.log(chalk4.yellow("\n\u2717 Cancelled\n"));
@@ -564,27 +696,54 @@ Unexpected error: ${error.message}
564
696
  // src/commands/status.ts
565
697
  import chalk6 from "chalk";
566
698
  import ora6 from "ora";
699
+ function getStatusColor2(status) {
700
+ switch (status) {
701
+ case "Backlog":
702
+ return chalk6.gray;
703
+ case "Todo":
704
+ return chalk6.yellow;
705
+ case "InProgress":
706
+ return chalk6.cyan;
707
+ case "Review":
708
+ return chalk6.magenta;
709
+ case "Done":
710
+ return chalk6.green;
711
+ default:
712
+ return chalk6.white;
713
+ }
714
+ }
567
715
  async function showStatus() {
568
716
  console.log(chalk6.bold("\n\u{1F4CA} Meno Status\n"));
569
- const selected = getSelectedProject();
717
+ const selectedTask = getSelectedTask();
570
718
  const timer = getActiveTimer();
571
- if (selected) {
719
+ if (selectedTask) {
720
+ const statusColor = getStatusColor2(selectedTask.status);
572
721
  console.log(
573
722
  chalk6.cyan(
574
- `\u{1F4CC} Selected: ${chalk6.bold(selected.name)} (${selected.clientName}) \u2022 $${selected.hourlyRate}/hr`
723
+ `\u{1F4CC} Selected: ${chalk6.bold(selectedTask.title)}`
575
724
  )
576
725
  );
726
+ console.log(
727
+ chalk6.dim(
728
+ ` ${selectedTask.projectName} \u2022 $${selectedTask.hourlyRate}/hr \u2022 `
729
+ ) + statusColor(selectedTask.status)
730
+ );
731
+ if (selectedTask.estimatedHours) {
732
+ console.log(chalk6.dim(` Estimated: ${selectedTask.estimatedHours}h`));
733
+ }
577
734
  }
578
735
  if (timer) {
579
736
  const elapsed = formatDuration(timer.startTime);
580
- const value = calculateTimerValue(timer.startTime, selected?.hourlyRate || 0);
737
+ const taskInfo = timer.taskTitle || timer.projectName;
738
+ const rate = selectedTask?.hourlyRate || 0;
739
+ const value = calculateTimerValue(timer.startTime, rate);
581
740
  console.log(
582
741
  chalk6.green(
583
- `\u23F1\uFE0F Running: ${chalk6.bold(timer.projectName)} \u2022 ${elapsed} \u2022 $${value.toFixed(2)}`
742
+ `\u23F1\uFE0F Running: ${chalk6.bold(taskInfo)} \u2022 ${elapsed} \u2022 $${value.toFixed(2)}`
584
743
  )
585
744
  );
586
745
  }
587
- if (selected || timer) {
746
+ if (selectedTask || timer) {
588
747
  console.log();
589
748
  }
590
749
  const spinner = ora6("Loading stats...").start();
@@ -614,13 +773,65 @@ Unexpected error: ${error.message}
614
773
  }
615
774
  }
616
775
 
776
+ // src/logo-ascii.ts
777
+ var SVG_EMBED = `<?xml version="1.0" encoding="UTF-8"?>
778
+ <svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 55.44 45.4">
779
+ <g id="MenoLogo">
780
+ <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"/>
781
+ <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"/>
782
+ <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"/>
783
+ </g>
784
+ </svg>`;
785
+ function fallbackAscii() {
786
+ return [
787
+ " ____ __ ",
788
+ " /\\ / __ \\/ /___ _ _ ",
789
+ " / \\ _ __ / / / / / __ \\ | | |",
790
+ " / /\\ \\ | '_ \\ / /_/ / / /_/ / |_| |",
791
+ " / ____ \\| | | |\\____/_/\\____/\\__,_|",
792
+ " /_/ \\_\\_| |_| ",
793
+ "",
794
+ " M E N O (logo) "
795
+ ].join("\n");
796
+ }
797
+ async function printLogoAscii(width = 64) {
798
+ try {
799
+ const sharpImport = await import("sharp");
800
+ const sharp = sharpImport.default ?? sharpImport;
801
+ const svgBuffer = Buffer.from(SVG_EMBED, "utf8");
802
+ const img = sharp(svgBuffer).resize({ width }).flatten({ background: { r: 255, g: 255, b: 255 } }).grayscale();
803
+ const { data, info } = await img.raw().toBuffer({ resolveWithObject: true });
804
+ const chars = " .,:;i1tfLCG08@";
805
+ const rows = [];
806
+ const aspect = 0.5;
807
+ const rowStep = Math.max(1, Math.round(1 / aspect));
808
+ for (let y = 0; y < info.height; y += rowStep) {
809
+ let line = "";
810
+ for (let x = 0; x < info.width; x++) {
811
+ const idx = y * info.width + x;
812
+ const val = data[idx];
813
+ const charIdx = Math.floor((1 - val / 255) * (chars.length - 1));
814
+ line += chars[charIdx];
815
+ }
816
+ rows.push(line);
817
+ }
818
+ console.log(rows.join("\n"));
819
+ return;
820
+ } catch (err) {
821
+ console.log(fallbackAscii());
822
+ return;
823
+ }
824
+ }
825
+
617
826
  // src/index.ts
618
827
  var program = new Command();
619
828
  program.name("meno").description("CLI for Meno time tracking").version("0.1.0");
829
+ printLogoAscii().catch(() => {
830
+ });
620
831
  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);
832
+ program.command("select").description("Select a task to work on").action(selectProject);
833
+ program.command("start [task-id]").description("Start a timer on selected task or specific task ID").action(startTimer);
834
+ 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
835
  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);
836
+ program.command("status").description("Show selected task, timer status, and unbilled stats").action(showStatus);
626
837
  program.parse();