@temet/cli 0.3.1 → 0.3.3

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,8 @@
1
+ TemetBar packaged app assets live here.
2
+
3
+ Expected artifact for `temet install-menubar`:
4
+
5
+ - `TemetBar.app.zip`
6
+
7
+ This artifact is produced from `packages/menubar-macos/` by the packaging
8
+ script described in `SPEC_29`.
Binary file
package/dist/audit.js CHANGED
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * temet audit — analyze your coding sessions, surface your real skills.
3
3
  */
4
- import { basename } from "node:path";
4
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { basename, join } from "node:path";
5
7
  import { clearLine, createInterface, cursorTo } from "node:readline";
6
8
  import { findSessionFiles, runAudit, } from "./lib/session-audit.js";
7
9
  import { resolveSessionPath, } from "./lib/path-resolver.js";
8
10
  import { trackAuditSnapshot, } from "./lib/audit-tracking.js";
11
+ import { loadMenubarState, resolveLastReportPath, writeMenubarState, } from "./lib/menubar-state.js";
9
12
  export function buildAuditJsonOutput(result, competencies, bilan, tracking) {
10
13
  return {
11
14
  sessions: result.sessionCount,
@@ -273,10 +276,11 @@ function printPretty(result, competencies, bilan, tracking) {
273
276
  console.log(`${dim("─".repeat(44), process.stdout)}`);
274
277
  console.log(`${BOLD}Next${RESET}`);
275
278
  if (!tracking) {
276
- console.log(` ${DIM}temet audit --path <dir> --track${RESET} Save a baseline and track changes`);
279
+ console.log(` ${DIM}temet audit --path <dir> --track${RESET} Save a baseline and track changes over time`);
277
280
  }
278
- console.log(` ${DIM}temet audit --path <dir> --json${RESET} Export as JSON`);
279
- console.log(` ${DIM}temet audit --path <dir> --publish${RESET} Publish to your Temet card`);
281
+ console.log(` ${DIM}temet traces${RESET} See the proof behind this reading`);
282
+ console.log(` ${DIM}temet plan${RESET} Turn this audit into a focused next-step plan`);
283
+ console.log(` ${DIM}temet audit --path <dir> --publish${RESET} Publish this audit to your Temet card`);
280
284
  console.log("");
281
285
  console.log(`${ok("Audit complete:", process.stdout)} ${formatCount(competencies.length)} skills surfaced from ${formatCount(result.sessionCount)} sessions`);
282
286
  if (tracking) {
@@ -284,7 +288,58 @@ function printPretty(result, competencies, bilan, tracking) {
284
288
  }
285
289
  console.log("");
286
290
  }
291
+ // ---------- Hook decline persistence ----------
292
+ const PREFS_PATH = join(homedir(), ".temet", "preferences.json");
293
+ function readPrefs() {
294
+ try {
295
+ return JSON.parse(readFileSync(PREFS_PATH, "utf8"));
296
+ }
297
+ catch {
298
+ return {};
299
+ }
300
+ }
301
+ function writePrefs(prefs) {
302
+ mkdirSync(join(homedir(), ".temet"), { recursive: true });
303
+ writeFileSync(PREFS_PATH, JSON.stringify(prefs, null, "\t") + "\n", "utf8");
304
+ }
305
+ function hasDeclinedHook() {
306
+ return readPrefs().hookDeclined === true;
307
+ }
308
+ function saveHookDeclined() {
309
+ writePrefs({ ...readPrefs(), hookDeclined: true });
310
+ }
311
+ function isMacOs() {
312
+ return process.platform === "darwin";
313
+ }
314
+ async function readHookInstalledSafe() {
315
+ try {
316
+ const hookModule = await import("./lib/hook-installer.js");
317
+ return hookModule.isHookInstalled(hookModule.readSettings(hookModule.getSettingsPath()));
318
+ }
319
+ catch {
320
+ return false;
321
+ }
322
+ }
287
323
  // ---------- Confirmation ----------
324
+ async function askYesNo(question, defaultYes, hintOverride) {
325
+ if (!process.stdin.isTTY)
326
+ return false;
327
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
328
+ const hint = hintOverride ??
329
+ (defaultYes
330
+ ? "Press Enter to continue, or type no to skip:"
331
+ : "Type yes to continue, or press Enter to cancel:");
332
+ return new Promise((resolve) => {
333
+ rl.question(`\n${BOLD}${question}${RESET}\n${dim(hint, process.stderr)} `, (answer) => {
334
+ rl.close();
335
+ const trimmed = answer.trim().toLowerCase();
336
+ if (trimmed === "")
337
+ resolve(defaultYes);
338
+ else
339
+ resolve(trimmed === "y" || trimmed === "yes");
340
+ });
341
+ });
342
+ }
288
343
  async function confirmPublish(competencyCount, address) {
289
344
  // Non-interactive (piped stdin) → refuse without --yes
290
345
  if (!process.stdin.isTTY) {
@@ -293,9 +348,10 @@ async function confirmPublish(competencyCount, address) {
293
348
  }
294
349
  const rl = createInterface({ input: process.stdin, output: process.stderr });
295
350
  return new Promise((resolve) => {
296
- rl.question(`\n${BOLD}Publish ${competencyCount} competencies to card ${address}?${RESET} [y/N] `, (answer) => {
351
+ rl.question(`\n${BOLD}Publish ${competencyCount} competencies to card ${address}?${RESET}\n${dim("Type yes to publish, or press Enter to cancel:", process.stderr)} `, (answer) => {
297
352
  rl.close();
298
- resolve(answer.trim().toLowerCase() === "y");
353
+ const trimmed = answer.trim().toLowerCase();
354
+ resolve(trimmed === "y" || trimmed === "yes" || trimmed === "publish");
299
355
  });
300
356
  });
301
357
  }
@@ -303,13 +359,13 @@ async function chooseSessionCandidate(candidates) {
303
359
  if (!process.stdin.isTTY || candidates.length === 0)
304
360
  return null;
305
361
  const rl = createInterface({ input: process.stdin, output: process.stderr });
306
- process.stderr.write(`\n[temet] I found ${candidates.length} Claude projects. Which one should I audit?\n\n`);
362
+ process.stderr.write(`\n[temet] I found ${candidates.length} Claude Code projects with session history. Pick one to audit.\n\n`);
307
363
  candidates.slice(0, 9).forEach((candidate, index) => {
308
364
  process.stderr.write(` ${index + 1}. ${candidate.label}${index === 0 ? " (most recent)" : ""}\n`);
309
365
  });
310
366
  process.stderr.write("\n");
311
367
  return new Promise((resolve) => {
312
- rl.question("[temet] Choose a project [1-9] or press Enter for the most recent: ", (answer) => {
368
+ rl.question("[temet] Choose 1-9, or press Enter for the most recent project: ", (answer) => {
313
369
  rl.close();
314
370
  const trimmed = answer.trim();
315
371
  if (!trimmed) {
@@ -372,9 +428,9 @@ export async function runAuditCommand(opts) {
372
428
  const commandStartedAt = Date.now();
373
429
  let resolvedPath = opts.path;
374
430
  if (!opts.quiet) {
375
- const resolution = await withSpinner(1, totalSteps, "Discovering sessions", async () => resolveSessionPath(opts.path || undefined, process.env));
431
+ const resolution = await withSpinner(1, totalSteps, "Finding your sessions", async () => resolveSessionPath(opts.path || undefined, process.env));
376
432
  if (!resolution.path) {
377
- console.error("[temet] no Claude sessions found. Run this command from a project folder, or use --path <session-dir>.");
433
+ console.error("[temet] No Claude Code sessions found here. Run this command from a project folder, or use --path <session-dir>.");
378
434
  process.exit(1);
379
435
  }
380
436
  if (!opts.path &&
@@ -403,7 +459,19 @@ export async function runAuditCommand(opts) {
403
459
  }
404
460
  resolvedPath = resolution.path;
405
461
  }
406
- const sessionFiles = findSessionFiles(resolvedPath);
462
+ let sessionFiles = findSessionFiles(resolvedPath);
463
+ if (sessionFiles.length === 0 && !opts.path && !opts.quiet && !opts.json) {
464
+ const { findClaudeProjectCandidates } = await import("./lib/path-resolver.js");
465
+ const candidates = findClaudeProjectCandidates();
466
+ if (candidates.length > 0) {
467
+ const selected = await chooseSessionCandidate(candidates);
468
+ if (selected) {
469
+ resolvedPath = selected.sessionDir;
470
+ sessionFiles = findSessionFiles(resolvedPath);
471
+ process.stderr.write(`${dim(`[temet] using ${selected.label}`, process.stderr)}\n`);
472
+ }
473
+ }
474
+ }
407
475
  if (sessionFiles.length === 0) {
408
476
  if (!opts.quiet) {
409
477
  console.error(`[temet] no .jsonl session files found in ${resolvedPath}`);
@@ -414,11 +482,11 @@ export async function runAuditCommand(opts) {
414
482
  printBanner();
415
483
  }
416
484
  if (!opts.quiet) {
417
- console.error(`[temet] scanning ${sessionFiles.length} session file(s)...`);
485
+ console.error(`[temet] reading ${sessionFiles.length} saved session file(s)...`);
418
486
  }
419
487
  let completedFiles = 0;
420
488
  if (!opts.quiet) {
421
- writeStepProgress(2, totalSteps, "Scanning sessions", 0, sessionFiles.length);
489
+ writeStepProgress(2, totalSteps, "Reading your sessions", 0, sessionFiles.length);
422
490
  }
423
491
  let scanDoneWritten = false;
424
492
  const result = await runAudit(sessionFiles, (event) => {
@@ -426,19 +494,19 @@ export async function runAuditCommand(opts) {
426
494
  return;
427
495
  if (event.phase === "scan") {
428
496
  completedFiles += 1;
429
- writeStepProgress(2, totalSteps, "Scanning sessions", completedFiles, sessionFiles.length, event.file ? basename(event.file) : undefined);
497
+ writeStepProgress(2, totalSteps, "Reading your sessions", completedFiles, sessionFiles.length, event.file ? basename(event.file) : undefined);
430
498
  return;
431
499
  }
432
500
  if (!scanDoneWritten) {
433
501
  scanDoneWritten = true;
434
- writeStepDone(2, totalSteps, "Scanning sessions", `${sessionFiles.length} files`);
502
+ writeStepDone(2, totalSteps, "Reading your sessions", `${sessionFiles.length} files`);
435
503
  }
436
504
  if (event.phase === "signals") {
437
505
  writeStepDone(3, totalSteps, "Extracting signals", `${formatCount(event.current ?? 0)} signals`, event.elapsedMs);
438
506
  return;
439
507
  }
440
508
  if (event.phase === "patterns") {
441
- writeStepDone(4, totalSteps, "Detecting patterns", `${formatCount(event.current ?? 0)} workflows`, event.elapsedMs);
509
+ writeStepDone(4, totalSteps, "Finding repeated patterns", `${formatCount(event.current ?? 0)} workflows`, event.elapsedMs);
442
510
  }
443
511
  });
444
512
  let { competencies } = result;
@@ -452,7 +520,7 @@ export async function runAuditCommand(opts) {
452
520
  printWarningBox("--narrate requires model access. Set ANTHROPIC_API_KEY or CLAUDE_AUTH_TOKEN.");
453
521
  }
454
522
  else {
455
- const narrated = await withSpinner(5, totalSteps, "Narrating profile", async () => narratorModule.narrateCompetencies(competencies, result.combined, result.workflows, { model: opts.model || undefined }));
523
+ const narrated = await withSpinner(5, totalSteps, "Writing your profile", async () => narratorModule.narrateCompetencies(competencies, result.combined, result.workflows, { model: opts.model || undefined }));
456
524
  competencies = narrated.competencies;
457
525
  bilan = narrated.bilan || undefined;
458
526
  }
@@ -463,13 +531,31 @@ export async function runAuditCommand(opts) {
463
531
  }
464
532
  if (opts.track) {
465
533
  tracking = await trackAuditSnapshot(resolvedPath, result, competencies);
466
- // OS notification (only behind --notify, and only if something changed)
467
- if (opts.notify && !tracking.skipped && tracking.changes.length > 0) {
468
- const { formatNotification, sendNotification } = await import("./lib/notifier.js");
469
- const payload = formatNotification(tracking.changes, tracking.current.projectLabel);
470
- if (payload) {
471
- void sendNotification(payload).catch(() => { });
472
- }
534
+ }
535
+ const projectLabel = tracking?.current.projectLabel ?? basename(resolvedPath);
536
+ let reportPath = null;
537
+ if (opts.openReport && !opts.json) {
538
+ const reportModule = await import("./lib/report-writer.js");
539
+ const content = reportModule.buildAuditTextReport(result, competencies, bilan, tracking);
540
+ reportPath = await reportModule.saveAuditTextReport(projectLabel, content);
541
+ }
542
+ const previousMenubarState = await loadMenubarState();
543
+ const hookInstalled = (await readHookInstalledSafe()) || opts.track;
544
+ const menubarState = await writeMenubarState({
545
+ sourcePath: resolvedPath,
546
+ result,
547
+ competencies,
548
+ bilan,
549
+ tracking,
550
+ trackingInstalled: hookInstalled,
551
+ lastReportPath: reportPath ?? (await resolveLastReportPath(projectLabel)),
552
+ previousState: previousMenubarState,
553
+ });
554
+ if (opts.notify && opts.track && tracking && !tracking.skipped) {
555
+ const { formatProgressNotification, sendNotification } = await import("./lib/notifier.js");
556
+ const payload = formatProgressNotification(menubarState.progress);
557
+ if (payload) {
558
+ void sendNotification(payload).catch(() => { });
473
559
  }
474
560
  }
475
561
  // Publish (runs even in quiet mode — it's a side-effect, not output)
@@ -506,7 +592,7 @@ export async function runAuditCommand(opts) {
506
592
  }
507
593
  }
508
594
  }
509
- // Quiet mode: no output
595
+ // Quiet mode: keep side-effects only
510
596
  if (opts.quiet)
511
597
  return;
512
598
  // Output
@@ -517,13 +603,56 @@ export async function runAuditCommand(opts) {
517
603
  else {
518
604
  printPretty(result, competencies, bilan, tracking);
519
605
  }
520
- if (opts.openReport && !opts.json) {
606
+ if (reportPath && !opts.json) {
521
607
  const reportModule = await import("./lib/report-writer.js");
522
- const content = reportModule.buildAuditTextReport(result, competencies, bilan, tracking);
523
- const projectLabel = tracking?.current.projectLabel ?? basename(resolvedPath);
524
- const filePath = await reportModule.saveAuditTextReport(projectLabel, content);
525
- await reportModule.openReportFile(filePath);
526
- console.error(`${dim(`[temet] opened report: ${filePath}`, process.stderr)}`);
608
+ await reportModule.openReportFile(reportPath);
609
+ console.error(`${dim(`[temet] opened report: ${reportPath}`, process.stderr)}`);
610
+ }
611
+ // Post-audit onboarding
612
+ if (!opts.json && !opts.quiet && process.stdin.isTTY) {
613
+ try {
614
+ const hookModule = await import("./lib/hook-installer.js");
615
+ const settingsPath = hookModule.getSettingsPath();
616
+ const settings = hookModule.readSettings(settingsPath);
617
+ const hookAlreadyInstalled = hookModule.isHookInstalled(settings);
618
+ if (!isMacOs()) {
619
+ if (!opts.track && !hookAlreadyInstalled && !hasDeclinedHook()) {
620
+ const answer = await askYesNo("Keep tracking your skills after each Claude Code session?", true);
621
+ if (answer) {
622
+ const binary = hookModule.resolveTemetBinary();
623
+ if (binary) {
624
+ const updated = hookModule.installHook(settings, binary);
625
+ hookModule.writeSettings(settingsPath, updated);
626
+ console.error(`${ok("Tracking enabled", process.stderr)} ${dim("You'll get a notification when your skills meaningfully change.", process.stderr)}`);
627
+ }
628
+ }
629
+ else {
630
+ saveHookDeclined();
631
+ }
632
+ }
633
+ }
634
+ else {
635
+ const { isMenubarInstalled, installMenubar } = await import("./lib/menubar-installer.js");
636
+ if (!isMenubarInstalled()) {
637
+ const answer = await askYesNo("Show Temet in your menu bar?", true, hookAlreadyInstalled
638
+ ? "Press Enter to install Temet, or type no to skip:"
639
+ : "Press Enter to install Temet and turn on automatic tracking, or type no to skip:");
640
+ if (answer) {
641
+ const result = await installMenubar({
642
+ ensureHook: !hookAlreadyInstalled,
643
+ });
644
+ const suffix = result.hookInstalled
645
+ ? "Temet opened and automatic tracking is now enabled."
646
+ : "Temet opened.";
647
+ console.error(`${ok("Temet installed", process.stderr)} ${dim(suffix, process.stderr)}`);
648
+ }
649
+ }
650
+ }
651
+ }
652
+ catch (error) {
653
+ const message = error instanceof Error ? error.message : String(error);
654
+ console.error(`${warn("Temet skipped", process.stderr)} ${dim(message, process.stderr)}`);
655
+ }
527
656
  }
528
657
  if (!opts.json) {
529
658
  console.error(`${ok("done", process.stderr)} ${dim(`in ${formatMs(Date.now() - commandStartedAt)}`, process.stderr)}`);
@@ -0,0 +1,5 @@
1
+ export type DoctorCliOptions = {
2
+ path?: string;
3
+ json: boolean;
4
+ };
5
+ export declare function runDoctorCommand(opts: DoctorCliOptions): Promise<void>;
package/dist/doctor.js ADDED
@@ -0,0 +1,79 @@
1
+ import { existsSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+ import { findClaudeProjectCandidates, resolveSessionPath, } from "./lib/path-resolver.js";
4
+ import { getSettingsPath, isHookInstalled, readSettings } from "./lib/hook-installer.js";
5
+ import { getMenubarAppPath, isMenubarInstalled } from "./lib/menubar-installer.js";
6
+ import { getMenubarConfigPath, getMenubarStatePath, } from "./lib/menubar-state.js";
7
+ import { resolveAuth } from "./lib/narrator-lite.js";
8
+ import { findSessionFiles } from "./lib/session-audit.js";
9
+ function buildCheck(id, label, status, detail) {
10
+ return { id, label, status, detail };
11
+ }
12
+ function iconFor(status) {
13
+ return status === "ok" ? "OK" : "WARN";
14
+ }
15
+ export async function runDoctorCommand(opts) {
16
+ const resolution = resolveSessionPath(opts.path || undefined, process.env);
17
+ const resolvedPath = resolution.path;
18
+ const sessionFiles = resolvedPath ? findSessionFiles(resolvedPath) : [];
19
+ const hookInstalled = isHookInstalled(readSettings(getSettingsPath()));
20
+ const narrationAvailable = resolveAuth() !== null;
21
+ const menubarInstalled = process.platform === "darwin" && isMenubarInstalled();
22
+ const menubarConfigExists = existsSync(getMenubarConfigPath());
23
+ const menubarStateExists = existsSync(getMenubarStatePath());
24
+ const candidates = findClaudeProjectCandidates();
25
+ const checks = [
26
+ buildCheck("sessions-root", "Claude Code projects", candidates.length > 0 ? "ok" : "warn", candidates.length > 0
27
+ ? `${candidates.length} project(s) with session history found`
28
+ : "No Claude Code project with session history was found"),
29
+ buildCheck("resolved-path", "Resolved session source", resolvedPath ? "ok" : "warn", resolvedPath
30
+ ? `${resolution.source}: ${basename(resolvedPath)}`
31
+ : "No session path could be resolved automatically"),
32
+ buildCheck("session-files", "Session files", sessionFiles.length > 0 ? "ok" : "warn", resolvedPath
33
+ ? `${sessionFiles.length} .jsonl file(s) in ${resolvedPath}`
34
+ : "No resolved project to scan"),
35
+ buildCheck("hook", "SessionEnd hook", hookInstalled ? "ok" : "warn", hookInstalled
36
+ ? `Installed in ${getSettingsPath()}`
37
+ : "Not installed. Run temet install-hook"),
38
+ buildCheck("narration", "Narration access", narrationAvailable ? "ok" : "warn", narrationAvailable
39
+ ? "Token detected for LLM narration"
40
+ : "No ANTHROPIC_API_KEY or CLAUDE_AUTH_TOKEN detected"),
41
+ buildCheck("menubar-app", "Temet app", process.platform !== "darwin" ? "warn" : menubarInstalled ? "ok" : "warn", process.platform !== "darwin"
42
+ ? "Temet app is only available on macOS"
43
+ : menubarInstalled
44
+ ? `Installed at ${getMenubarAppPath()}`
45
+ : "Not installed. Run temet install-menubar"),
46
+ buildCheck("menubar-config", "Temet app config", process.platform !== "darwin"
47
+ ? "warn"
48
+ : menubarConfigExists
49
+ ? "ok"
50
+ : "warn", process.platform !== "darwin"
51
+ ? "Not applicable on this platform"
52
+ : menubarConfigExists
53
+ ? `Config present at ${getMenubarConfigPath()}`
54
+ : "No menubar config found yet"),
55
+ buildCheck("menubar-state", "Temet app state", menubarStateExists ? "ok" : "warn", menubarStateExists
56
+ ? `Sidecar present at ${getMenubarStatePath()}`
57
+ : "No menubar state written yet. Run temet audit --track"),
58
+ ];
59
+ const report = {
60
+ version: 1,
61
+ checks,
62
+ summary: {
63
+ ok: checks.filter((check) => check.status === "ok").length,
64
+ warn: checks.filter((check) => check.status === "warn").length,
65
+ },
66
+ };
67
+ if (opts.json) {
68
+ console.log(JSON.stringify(report, null, "\t"));
69
+ return;
70
+ }
71
+ console.log("Temet doctor");
72
+ console.log("");
73
+ for (const check of checks) {
74
+ console.log(`${iconFor(check.status)} ${check.label}`);
75
+ console.log(` ${check.detail}`);
76
+ }
77
+ console.log("");
78
+ console.log(`Summary: ${report.summary.ok} OK, ${report.summary.warn} warning${report.summary.warn === 1 ? "" : "s"}`);
79
+ }
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile } from "node:child_process";
3
+ import { readFileSync } from "node:fs";
3
4
  import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
5
  import { homedir } from "node:os";
5
6
  import { dirname, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
6
8
  import { promisify } from "node:util";
7
9
  import { buildAuditCliOptions, parseFlagBag, readOptionalString, } from "./lib/cli-args.js";
8
10
  const execFileAsync = promisify(execFile);
@@ -11,12 +13,28 @@ const DEFAULT_SERVER_NAME = "temet";
11
13
  const DEFAULT_PROTOCOL_APP_NAME = "Temet Handler";
12
14
  const LINUX_DESKTOP_ENTRY = "temet-handler.desktop";
13
15
  const LINUX_HANDLER_SCRIPT = "temet-protocol-handler";
14
- const HELP = `Temet CLI — discover the skills you already demonstrate in AI work
16
+ const PACKAGE_JSON_PATH = fileURLToPath(new URL("../package.json", import.meta.url));
17
+ function readCliVersion() {
18
+ try {
19
+ const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, "utf8"));
20
+ return packageJson.version || "0.0.0";
21
+ }
22
+ catch {
23
+ return "0.0.0";
24
+ }
25
+ }
26
+ const CLI_VERSION = readCliVersion();
27
+ const HELP = `Temet CLI ${CLI_VERSION} — discover the skills you already demonstrate in AI work
15
28
 
16
29
  Commands:
17
30
  \ttemet audit [--path <session-dir>] Analyze local sessions, surface skills and workflows
31
+ \ttemet doctor [--path <session-dir>] Check local Temet setup, sessions, hook, and app state
32
+ \ttemet traces [--path <session-dir>] Show the evidence behind the strongest signals
33
+ \ttemet plan [--path <session-dir>] Turn the current audit into an operating plan
18
34
  \ttemet install-hook Auto-audit after every Claude Code session
19
35
  \ttemet uninstall-hook Remove the SessionEnd hook
36
+ \ttemet install-menubar Install Temet in the macOS menu bar
37
+ \ttemet uninstall-menubar Remove Temet from the macOS menu bar
20
38
  \ttemet connect --address <hex> --token <t> Connect your MCP client to your Temet card
21
39
  \ttemet install-handler Register temet:// protocol handler
22
40
 
@@ -32,21 +50,30 @@ Audit options:
32
50
 
33
51
  Advanced:
34
52
  \t--narrate Enrich results with an LLM (requires model access: ANTHROPIC_API_KEY or CLAUDE_AUTH_TOKEN)
53
+ \t--no-narrate Disable automatic narration even if model access is available
35
54
  \t--model <id> Model to use for narration (default: claude-haiku-4-5-20251001)
36
55
 
37
56
  Examples:
38
57
  \ttemet audit Auto-detect sessions from cwd
58
+ \ttemet doctor Check whether Temet is fully wired on this machine
39
59
  \ttemet audit Open the text report by default
60
+ \ttemet traces Inspect the proof behind the current reading
61
+ \ttemet plan Get the current operating plan
40
62
  \ttemet audit --path ~/.claude/projects/my-project
41
63
  \ttemet audit --path ~/.claude/projects/my-project --track
42
64
  \ttemet audit --path ~/.claude/projects/my-project --open-report
43
65
  \ttemet audit --path ~/.claude/projects/my-project --json
44
66
  \ttemet install-hook Background audit on session end
67
+ \ttemet install-menubar Install and open Temet on macOS
45
68
  `;
46
69
  function printHelp(exitCode = 0) {
47
70
  console.log(HELP);
48
71
  process.exit(exitCode);
49
72
  }
73
+ function printVersion() {
74
+ console.log(CLI_VERSION);
75
+ process.exit(0);
76
+ }
50
77
  function isObject(value) {
51
78
  return typeof value === "object" && value !== null && !Array.isArray(value);
52
79
  }
@@ -139,6 +166,9 @@ function parseConnectUrlOptions(flags, positionals) {
139
166
  };
140
167
  }
141
168
  function parseArgs(argv) {
169
+ if (argv.includes("-v") || argv.includes("--version")) {
170
+ printVersion();
171
+ }
142
172
  if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
143
173
  printHelp(0);
144
174
  }
@@ -151,6 +181,21 @@ function parseArgs(argv) {
151
181
  options: buildAuditCliOptions(flags, process.env),
152
182
  };
153
183
  }
184
+ if (command === "doctor") {
185
+ return {
186
+ command,
187
+ options: {
188
+ path: readOptionalString(flags, "path") ?? undefined,
189
+ json: Boolean(flags.get("json")),
190
+ },
191
+ };
192
+ }
193
+ if (command === "traces" || command === "plan") {
194
+ return {
195
+ command,
196
+ options: buildAuditCliOptions(flags, process.env),
197
+ };
198
+ }
154
199
  if (command === "connect") {
155
200
  return {
156
201
  command,
@@ -169,6 +214,9 @@ function parseArgs(argv) {
169
214
  if (command === "install-hook" || command === "uninstall-hook") {
170
215
  return { command, dryRun };
171
216
  }
217
+ if (command === "install-menubar" || command === "uninstall-menubar") {
218
+ return { command, dryRun };
219
+ }
172
220
  console.error(`Unknown command: ${command}`);
173
221
  printHelp(1);
174
222
  }
@@ -494,6 +542,21 @@ async function run() {
494
542
  await runAuditCommand(parsed.options);
495
543
  return;
496
544
  }
545
+ if (parsed.command === "doctor") {
546
+ const { runDoctorCommand } = await import("./doctor.js");
547
+ await runDoctorCommand(parsed.options);
548
+ return;
549
+ }
550
+ if (parsed.command === "traces") {
551
+ const { runTracesCommand } = await import("./traces.js");
552
+ await runTracesCommand(parsed.options);
553
+ return;
554
+ }
555
+ if (parsed.command === "plan") {
556
+ const { runPlanCommand } = await import("./plan.js");
557
+ await runPlanCommand(parsed.options);
558
+ return;
559
+ }
497
560
  if (parsed.command === "connect" || parsed.command === "connect-url") {
498
561
  await runWriteFlow(parsed.command, parsed.options);
499
562
  return;
@@ -540,6 +603,35 @@ async function run() {
540
603
  console.log("[temet] SessionEnd hook removed.");
541
604
  return;
542
605
  }
606
+ if (parsed.command === "install-menubar") {
607
+ const { installMenubar } = await import("./lib/menubar-installer.js");
608
+ const result = await installMenubar({
609
+ dryRun: parsed.dryRun,
610
+ ensureHook: true,
611
+ });
612
+ if (parsed.dryRun) {
613
+ console.log(`[temet] dry-run menubar install path: ${result.appPath}`);
614
+ console.log(`[temet] dry-run menubar config path: ${result.configPath}`);
615
+ return;
616
+ }
617
+ console.log(`[temet] Temet installed: ${result.appPath}`);
618
+ if (result.hookInstalled) {
619
+ console.log("[temet] Tracking enabled via SessionEnd hook.");
620
+ }
621
+ console.log("[temet] Temet opened.");
622
+ return;
623
+ }
624
+ if (parsed.command === "uninstall-menubar") {
625
+ const { uninstallMenubar } = await import("./lib/menubar-installer.js");
626
+ const result = await uninstallMenubar({ dryRun: parsed.dryRun });
627
+ if (parsed.dryRun) {
628
+ console.log(`[temet] dry-run menubar uninstall path: ${result.appPath}`);
629
+ return;
630
+ }
631
+ console.log("[temet] Temet removed.");
632
+ console.log("[temet] Tracking remains active — run temet uninstall-hook to disable it.");
633
+ return;
634
+ }
543
635
  if (parsed.command === "install-handler") {
544
636
  await installProtocolHandler(parsed.dryRun);
545
637
  console.log("[temet] next: click a Quick connect button in Temet.");
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
+ import { humanizeSessionDir } from "./path-resolver.js";
5
6
  const PROFICIENCY_ORDER = {
6
7
  novice: 0,
7
8
  advanced_beginner: 1,
@@ -26,8 +27,18 @@ export function buildProjectKey(sourcePath) {
26
27
  }
27
28
  function buildProjectLabel(sourcePath) {
28
29
  const resolved = path.resolve(sourcePath);
29
- const base = path.basename(resolved);
30
- return base || resolved;
30
+ const slug = path.basename(resolved);
31
+ if (slug.startsWith("-")) {
32
+ const segments = slug.slice(1).split("-").filter(Boolean);
33
+ if (segments.length >= 2) {
34
+ return `${segments[segments.length - 2]}-${segments[segments.length - 1]}`;
35
+ }
36
+ if (segments.length === 1) {
37
+ return segments[0];
38
+ }
39
+ }
40
+ const humanized = humanizeSessionDir(resolved);
41
+ return path.basename(humanized) || path.basename(resolved) || resolved;
31
42
  }
32
43
  export function buildAuditSnapshot(sourcePath, result, competencies) {
33
44
  return {
@@ -14,6 +14,7 @@ export function parseFlagBag(args) {
14
14
  if (arg === "--dry-run" ||
15
15
  arg === "--track" ||
16
16
  arg === "--narrate" ||
17
+ arg === "--no-narrate" ||
17
18
  arg === "--open-report" ||
18
19
  arg === "--json" ||
19
20
  arg === "--quiet" ||
@@ -48,7 +49,11 @@ export function buildAuditCliOptions(flags, env) {
48
49
  return {
49
50
  path: pathVal,
50
51
  track: Boolean(flags.get("track")),
51
- narrate: Boolean(flags.get("narrate")),
52
+ narrate: !Boolean(flags.get("no-narrate")) &&
53
+ (Boolean(flags.get("narrate")) ||
54
+ (!json &&
55
+ !quiet &&
56
+ Boolean(env.CLAUDE_AUTH_TOKEN || env.ANTHROPIC_API_KEY))),
52
57
  openReport: explicitOpenReport || (!json && !quiet),
53
58
  json,
54
59
  quiet,
@@ -0,0 +1,13 @@
1
+ import type { AuditCliOptions } from "./cli-args.js";
2
+ import { type TrackingResult } from "./audit-tracking.js";
3
+ import { type AuditResult } from "./session-audit.js";
4
+ import type { CompetencyEntry } from "./types.js";
5
+ export type ResolvedDiagnostics = {
6
+ resolvedPath: string;
7
+ result: AuditResult;
8
+ competencies: CompetencyEntry[];
9
+ bilan?: string;
10
+ tracking?: TrackingResult;
11
+ };
12
+ export declare function resolveDiagnostics(opts: AuditCliOptions): Promise<ResolvedDiagnostics>;
13
+ export declare function defaultProjectLabel(resolvedPath: string): string;