@temet/cli 0.3.0 → 0.3.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.
@@ -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.d.ts CHANGED
@@ -5,6 +5,7 @@ export type AuditOptions = {
5
5
  path: string;
6
6
  track: boolean;
7
7
  narrate: boolean;
8
+ openReport: boolean;
8
9
  json: boolean;
9
10
  quiet: boolean;
10
11
  notify: boolean;
@@ -15,7 +16,7 @@ export type AuditOptions = {
15
16
  token: string;
16
17
  relayUrl: string;
17
18
  };
18
- export declare function buildAuditJsonOutput(result: Pick<AuditResult, "sessionCount" | "messageCount" | "toolCallCount" | "workflows">, competencies: CompetencyEntry[], bilan?: string, tracking?: Pick<TrackingResult, "changes" | "latestPath">): {
19
+ export declare function buildAuditJsonOutput(result: Pick<AuditResult, "sessionCount" | "messageCount" | "promptCount" | "toolCallCount" | "workflows">, competencies: CompetencyEntry[], bilan?: string, tracking?: Pick<TrackingResult, "changes" | "latestPath">): {
19
20
  tracking?: {
20
21
  latestPath: string;
21
22
  changes: AuditChange[];
@@ -23,6 +24,7 @@ export declare function buildAuditJsonOutput(result: Pick<AuditResult, "sessionC
23
24
  bilan?: string | undefined;
24
25
  sessions: number;
25
26
  messages: number;
27
+ prompts: number;
26
28
  toolCalls: number;
27
29
  competencies: CompetencyEntry[];
28
30
  workflows: import("./lib/workflow-detector.js").DetectedWorkflow[];
package/dist/audit.js CHANGED
@@ -1,14 +1,19 @@
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";
9
+ import { resolveSessionPath, } from "./lib/path-resolver.js";
7
10
  import { trackAuditSnapshot, } from "./lib/audit-tracking.js";
11
+ import { loadMenubarState, resolveLastReportPath, writeMenubarState, } from "./lib/menubar-state.js";
8
12
  export function buildAuditJsonOutput(result, competencies, bilan, tracking) {
9
13
  return {
10
14
  sessions: result.sessionCount,
11
15
  messages: result.messageCount,
16
+ prompts: result.promptCount,
12
17
  toolCalls: result.toolCallCount,
13
18
  competencies,
14
19
  workflows: result.workflows,
@@ -271,10 +276,11 @@ function printPretty(result, competencies, bilan, tracking) {
271
276
  console.log(`${dim("─".repeat(44), process.stdout)}`);
272
277
  console.log(`${BOLD}Next${RESET}`);
273
278
  if (!tracking) {
274
- 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`);
275
280
  }
276
- console.log(` ${DIM}temet audit --path <dir> --json${RESET} Export as JSON`);
277
- 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`);
278
284
  console.log("");
279
285
  console.log(`${ok("Audit complete:", process.stdout)} ${formatCount(competencies.length)} skills surfaced from ${formatCount(result.sessionCount)} sessions`);
280
286
  if (tracking) {
@@ -282,7 +288,58 @@ function printPretty(result, competencies, bilan, tracking) {
282
288
  }
283
289
  console.log("");
284
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
+ }
285
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
+ }
286
343
  async function confirmPublish(competencyCount, address) {
287
344
  // Non-interactive (piped stdin) → refuse without --yes
288
345
  if (!process.stdin.isTTY) {
@@ -291,9 +348,38 @@ async function confirmPublish(competencyCount, address) {
291
348
  }
292
349
  const rl = createInterface({ input: process.stdin, output: process.stderr });
293
350
  return new Promise((resolve) => {
294
- 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) => {
295
352
  rl.close();
296
- resolve(answer.trim().toLowerCase() === "y");
353
+ const trimmed = answer.trim().toLowerCase();
354
+ resolve(trimmed === "y" || trimmed === "yes" || trimmed === "publish");
355
+ });
356
+ });
357
+ }
358
+ async function chooseSessionCandidate(candidates) {
359
+ if (!process.stdin.isTTY || candidates.length === 0)
360
+ return null;
361
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
362
+ process.stderr.write(`\n[temet] I found ${candidates.length} Claude Code projects with session history. Pick one to audit.\n\n`);
363
+ candidates.slice(0, 9).forEach((candidate, index) => {
364
+ process.stderr.write(` ${index + 1}. ${candidate.label}${index === 0 ? " (most recent)" : ""}\n`);
365
+ });
366
+ process.stderr.write("\n");
367
+ return new Promise((resolve) => {
368
+ rl.question("[temet] Choose 1-9, or press Enter for the most recent project: ", (answer) => {
369
+ rl.close();
370
+ const trimmed = answer.trim();
371
+ if (!trimmed) {
372
+ resolve(candidates[0] ?? null);
373
+ return;
374
+ }
375
+ const index = Number.parseInt(trimmed, 10) - 1;
376
+ if (!Number.isFinite(index) ||
377
+ index < 0 ||
378
+ index >= candidates.length) {
379
+ resolve(candidates[0] ?? null);
380
+ return;
381
+ }
382
+ resolve(candidates[index] ?? null);
297
383
  });
298
384
  });
299
385
  }
@@ -338,10 +424,45 @@ async function publishCompetencies(competencies, opts) {
338
424
  }
339
425
  // ---------- Main ----------
340
426
  export async function runAuditCommand(opts) {
341
- const sessionFiles = findSessionFiles(opts.path);
427
+ const totalSteps = 4 + (opts.narrate ? 1 : 0) + (opts.publish ? 1 : 0);
428
+ const commandStartedAt = Date.now();
429
+ let resolvedPath = opts.path;
430
+ if (!opts.quiet) {
431
+ const resolution = await withSpinner(1, totalSteps, "Finding your sessions", async () => resolveSessionPath(opts.path || undefined, process.env));
432
+ if (!resolution.path) {
433
+ console.error("[temet] No Claude Code sessions found here. Run this command from a project folder, or use --path <session-dir>.");
434
+ process.exit(1);
435
+ }
436
+ if (!opts.path &&
437
+ !opts.json &&
438
+ resolution.source === "recent" &&
439
+ resolution.candidates.length > 1) {
440
+ const selected = await chooseSessionCandidate(resolution.candidates);
441
+ resolvedPath = selected?.sessionDir ?? resolution.path;
442
+ const label = selected?.label ?? resolution.candidates[0]?.label;
443
+ if (label) {
444
+ process.stderr.write(`${dim(`[temet] using ${label}`, process.stderr)}\n`);
445
+ }
446
+ }
447
+ else {
448
+ resolvedPath = resolution.path;
449
+ const label = resolution.candidates[0]?.label;
450
+ if (label && resolution.source !== "explicit") {
451
+ process.stderr.write(`${dim(`[temet] using ${label}`, process.stderr)}\n`);
452
+ }
453
+ }
454
+ }
455
+ else {
456
+ const resolution = resolveSessionPath(opts.path || undefined, process.env);
457
+ if (!resolution.path) {
458
+ process.exit(1);
459
+ }
460
+ resolvedPath = resolution.path;
461
+ }
462
+ const sessionFiles = findSessionFiles(resolvedPath);
342
463
  if (sessionFiles.length === 0) {
343
464
  if (!opts.quiet) {
344
- console.error(`[temet] no .jsonl session files found in ${opts.path}`);
465
+ console.error(`[temet] no .jsonl session files found in ${resolvedPath}`);
345
466
  }
346
467
  process.exit(1);
347
468
  }
@@ -349,13 +470,11 @@ export async function runAuditCommand(opts) {
349
470
  printBanner();
350
471
  }
351
472
  if (!opts.quiet) {
352
- console.error(`[temet] scanning ${sessionFiles.length} session file(s)...`);
473
+ console.error(`[temet] reading ${sessionFiles.length} saved session file(s)...`);
353
474
  }
354
- const totalSteps = 3 + (opts.narrate ? 1 : 0) + (opts.publish ? 1 : 0);
355
475
  let completedFiles = 0;
356
- const commandStartedAt = Date.now();
357
476
  if (!opts.quiet) {
358
- writeStepProgress(1, totalSteps, "Scanning sessions", 0, sessionFiles.length);
477
+ writeStepProgress(2, totalSteps, "Reading your sessions", 0, sessionFiles.length);
359
478
  }
360
479
  let scanDoneWritten = false;
361
480
  const result = await runAudit(sessionFiles, (event) => {
@@ -363,19 +482,19 @@ export async function runAuditCommand(opts) {
363
482
  return;
364
483
  if (event.phase === "scan") {
365
484
  completedFiles += 1;
366
- writeStepProgress(1, totalSteps, "Scanning sessions", completedFiles, sessionFiles.length, event.file ? basename(event.file) : undefined);
485
+ writeStepProgress(2, totalSteps, "Reading your sessions", completedFiles, sessionFiles.length, event.file ? basename(event.file) : undefined);
367
486
  return;
368
487
  }
369
488
  if (!scanDoneWritten) {
370
489
  scanDoneWritten = true;
371
- writeStepDone(1, totalSteps, "Scanning sessions", `${sessionFiles.length} files`);
490
+ writeStepDone(2, totalSteps, "Reading your sessions", `${sessionFiles.length} files`);
372
491
  }
373
492
  if (event.phase === "signals") {
374
- writeStepDone(2, totalSteps, "Extracting signals", `${formatCount(event.current ?? 0)} signals`, event.elapsedMs);
493
+ writeStepDone(3, totalSteps, "Extracting signals", `${formatCount(event.current ?? 0)} signals`, event.elapsedMs);
375
494
  return;
376
495
  }
377
496
  if (event.phase === "patterns") {
378
- writeStepDone(3, totalSteps, "Detecting patterns", `${formatCount(event.current ?? 0)} workflows`, event.elapsedMs);
497
+ writeStepDone(4, totalSteps, "Finding repeated patterns", `${formatCount(event.current ?? 0)} workflows`, event.elapsedMs);
379
498
  }
380
499
  });
381
500
  let { competencies } = result;
@@ -389,7 +508,7 @@ export async function runAuditCommand(opts) {
389
508
  printWarningBox("--narrate requires model access. Set ANTHROPIC_API_KEY or CLAUDE_AUTH_TOKEN.");
390
509
  }
391
510
  else {
392
- const narrated = await withSpinner(4, totalSteps, "Narrating profile", async () => narratorModule.narrateCompetencies(competencies, result.combined, result.workflows, { model: opts.model || undefined }));
511
+ const narrated = await withSpinner(5, totalSteps, "Writing your profile", async () => narratorModule.narrateCompetencies(competencies, result.combined, result.workflows, { model: opts.model || undefined }));
393
512
  competencies = narrated.competencies;
394
513
  bilan = narrated.bilan || undefined;
395
514
  }
@@ -399,14 +518,32 @@ export async function runAuditCommand(opts) {
399
518
  }
400
519
  }
401
520
  if (opts.track) {
402
- tracking = await trackAuditSnapshot(opts.path, result, competencies);
403
- // OS notification (only behind --notify, and only if something changed)
404
- if (opts.notify && !tracking.skipped && tracking.changes.length > 0) {
405
- const { formatNotification, sendNotification } = await import("./lib/notifier.js");
406
- const payload = formatNotification(tracking.changes, tracking.current.projectLabel);
407
- if (payload) {
408
- void sendNotification(payload).catch(() => { });
409
- }
521
+ tracking = await trackAuditSnapshot(resolvedPath, result, competencies);
522
+ }
523
+ const projectLabel = tracking?.current.projectLabel ?? basename(resolvedPath);
524
+ let reportPath = null;
525
+ if (opts.openReport && !opts.json) {
526
+ const reportModule = await import("./lib/report-writer.js");
527
+ const content = reportModule.buildAuditTextReport(result, competencies, bilan, tracking);
528
+ reportPath = await reportModule.saveAuditTextReport(projectLabel, content);
529
+ }
530
+ const previousMenubarState = await loadMenubarState();
531
+ const hookInstalled = (await readHookInstalledSafe()) || opts.track;
532
+ const menubarState = await writeMenubarState({
533
+ sourcePath: resolvedPath,
534
+ result,
535
+ competencies,
536
+ bilan,
537
+ tracking,
538
+ trackingInstalled: hookInstalled,
539
+ lastReportPath: reportPath ?? (await resolveLastReportPath(projectLabel)),
540
+ previousState: previousMenubarState,
541
+ });
542
+ if (opts.notify && opts.track && tracking && !tracking.skipped) {
543
+ const { formatProgressNotification, sendNotification } = await import("./lib/notifier.js");
544
+ const payload = formatProgressNotification(menubarState.progress);
545
+ if (payload) {
546
+ void sendNotification(payload).catch(() => { });
410
547
  }
411
548
  }
412
549
  // Publish (runs even in quiet mode — it's a side-effect, not output)
@@ -443,7 +580,7 @@ export async function runAuditCommand(opts) {
443
580
  }
444
581
  }
445
582
  }
446
- // Quiet mode: no output
583
+ // Quiet mode: keep side-effects only
447
584
  if (opts.quiet)
448
585
  return;
449
586
  // Output
@@ -454,6 +591,57 @@ export async function runAuditCommand(opts) {
454
591
  else {
455
592
  printPretty(result, competencies, bilan, tracking);
456
593
  }
594
+ if (reportPath && !opts.json) {
595
+ const reportModule = await import("./lib/report-writer.js");
596
+ await reportModule.openReportFile(reportPath);
597
+ console.error(`${dim(`[temet] opened report: ${reportPath}`, process.stderr)}`);
598
+ }
599
+ // Post-audit onboarding
600
+ if (!opts.json && !opts.quiet && process.stdin.isTTY) {
601
+ try {
602
+ const hookModule = await import("./lib/hook-installer.js");
603
+ const settingsPath = hookModule.getSettingsPath();
604
+ const settings = hookModule.readSettings(settingsPath);
605
+ const hookAlreadyInstalled = hookModule.isHookInstalled(settings);
606
+ if (!isMacOs()) {
607
+ if (!opts.track && !hookAlreadyInstalled && !hasDeclinedHook()) {
608
+ const answer = await askYesNo("Keep tracking your skills after each Claude Code session?", true);
609
+ if (answer) {
610
+ const binary = hookModule.resolveTemetBinary();
611
+ if (binary) {
612
+ const updated = hookModule.installHook(settings, binary);
613
+ hookModule.writeSettings(settingsPath, updated);
614
+ console.error(`${ok("Tracking enabled", process.stderr)} ${dim("You'll get a notification when your skills meaningfully change.", process.stderr)}`);
615
+ }
616
+ }
617
+ else {
618
+ saveHookDeclined();
619
+ }
620
+ }
621
+ }
622
+ else {
623
+ const { isMenubarInstalled, installMenubar } = await import("./lib/menubar-installer.js");
624
+ if (!isMenubarInstalled()) {
625
+ const answer = await askYesNo("Show Temet in your menu bar?", true, hookAlreadyInstalled
626
+ ? "Press Enter to install Temet, or type no to skip:"
627
+ : "Press Enter to install Temet and turn on automatic tracking, or type no to skip:");
628
+ if (answer) {
629
+ const result = await installMenubar({
630
+ ensureHook: !hookAlreadyInstalled,
631
+ });
632
+ const suffix = result.hookInstalled
633
+ ? "Temet opened and automatic tracking is now enabled."
634
+ : "Temet opened.";
635
+ console.error(`${ok("Temet installed", process.stderr)} ${dim(suffix, process.stderr)}`);
636
+ }
637
+ }
638
+ }
639
+ }
640
+ catch (error) {
641
+ const message = error instanceof Error ? error.message : String(error);
642
+ console.error(`${warn("Temet skipped", process.stderr)} ${dim(message, process.stderr)}`);
643
+ }
644
+ }
457
645
  if (!opts.json) {
458
646
  console.error(`${ok("done", process.stderr)} ${dim(`in ${formatMs(Date.now() - commandStartedAt)}`, process.stderr)}`);
459
647
  }
package/dist/index.js CHANGED
@@ -5,7 +5,6 @@ import { homedir } from "node:os";
5
5
  import { dirname, resolve } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { buildAuditCliOptions, parseFlagBag, readOptionalString, } from "./lib/cli-args.js";
8
- import { resolveSessionPath } from "./lib/path-resolver.js";
9
8
  const execFileAsync = promisify(execFile);
10
9
  const DEFAULT_RELAY_URL = "https://temet-relay.ramponneau.workers.dev/mcp";
11
10
  const DEFAULT_SERVER_NAME = "temet";
@@ -16,14 +15,19 @@ const HELP = `Temet CLI — discover the skills you already demonstrate in AI wo
16
15
 
17
16
  Commands:
18
17
  \ttemet audit [--path <session-dir>] Analyze local sessions, surface skills and workflows
18
+ \ttemet traces [--path <session-dir>] Show the evidence behind the strongest signals
19
+ \ttemet plan [--path <session-dir>] Turn the current audit into an operating plan
19
20
  \ttemet install-hook Auto-audit after every Claude Code session
20
21
  \ttemet uninstall-hook Remove the SessionEnd hook
22
+ \ttemet install-menubar Install Temet in the macOS menu bar
23
+ \ttemet uninstall-menubar Remove Temet from the macOS menu bar
21
24
  \ttemet connect --address <hex> --token <t> Connect your MCP client to your Temet card
22
25
  \ttemet install-handler Register temet:// protocol handler
23
26
 
24
27
  Audit options:
25
28
  \t--path <dir> Directory containing .jsonl session files (auto-detected if omitted)
26
29
  \t--track Save a local snapshot and compare against the previous audit
30
+ \t--open-report Explicitly save and open the text report (opened by default unless --json or --quiet)
27
31
  \t--json Output structured JSON instead of terminal display
28
32
  \t--quiet Suppress all output (for background hooks)
29
33
  \t--notify Send an OS notification on skill changes (used with --track)
@@ -32,14 +36,20 @@ Audit options:
32
36
 
33
37
  Advanced:
34
38
  \t--narrate Enrich results with an LLM (requires model access: ANTHROPIC_API_KEY or CLAUDE_AUTH_TOKEN)
39
+ \t--no-narrate Disable automatic narration even if model access is available
35
40
  \t--model <id> Model to use for narration (default: claude-haiku-4-5-20251001)
36
41
 
37
42
  Examples:
38
43
  \ttemet audit Auto-detect sessions from cwd
44
+ \ttemet audit Open the text report by default
45
+ \ttemet traces Inspect the proof behind the current reading
46
+ \ttemet plan Get the current operating plan
39
47
  \ttemet audit --path ~/.claude/projects/my-project
40
48
  \ttemet audit --path ~/.claude/projects/my-project --track
49
+ \ttemet audit --path ~/.claude/projects/my-project --open-report
41
50
  \ttemet audit --path ~/.claude/projects/my-project --json
42
51
  \ttemet install-hook Background audit on session end
52
+ \ttemet install-menubar Install and open Temet on macOS
43
53
  `;
44
54
  function printHelp(exitCode = 0) {
45
55
  console.log(HELP);
@@ -144,19 +154,15 @@ function parseArgs(argv) {
144
154
  const { flags, positionals } = parseFlagBag(rest);
145
155
  const dryRun = Boolean(flags.get("dry-run"));
146
156
  if (command === "audit") {
147
- const options = buildAuditCliOptions(flags, process.env);
148
- if (!options.path) {
149
- // Auto-detect session path
150
- const detected = resolveSessionPath(undefined, process.env);
151
- if (!detected) {
152
- console.error("[temet] could not auto-detect session directory. Use --path <session-dir>");
153
- process.exit(1);
154
- }
155
- options.path = detected;
156
- }
157
157
  return {
158
158
  command,
159
- options,
159
+ options: buildAuditCliOptions(flags, process.env),
160
+ };
161
+ }
162
+ if (command === "traces" || command === "plan") {
163
+ return {
164
+ command,
165
+ options: buildAuditCliOptions(flags, process.env),
160
166
  };
161
167
  }
162
168
  if (command === "connect") {
@@ -177,6 +183,9 @@ function parseArgs(argv) {
177
183
  if (command === "install-hook" || command === "uninstall-hook") {
178
184
  return { command, dryRun };
179
185
  }
186
+ if (command === "install-menubar" || command === "uninstall-menubar") {
187
+ return { command, dryRun };
188
+ }
180
189
  console.error(`Unknown command: ${command}`);
181
190
  printHelp(1);
182
191
  }
@@ -502,6 +511,16 @@ async function run() {
502
511
  await runAuditCommand(parsed.options);
503
512
  return;
504
513
  }
514
+ if (parsed.command === "traces") {
515
+ const { runTracesCommand } = await import("./traces.js");
516
+ await runTracesCommand(parsed.options);
517
+ return;
518
+ }
519
+ if (parsed.command === "plan") {
520
+ const { runPlanCommand } = await import("./plan.js");
521
+ await runPlanCommand(parsed.options);
522
+ return;
523
+ }
505
524
  if (parsed.command === "connect" || parsed.command === "connect-url") {
506
525
  await runWriteFlow(parsed.command, parsed.options);
507
526
  return;
@@ -548,6 +567,35 @@ async function run() {
548
567
  console.log("[temet] SessionEnd hook removed.");
549
568
  return;
550
569
  }
570
+ if (parsed.command === "install-menubar") {
571
+ const { installMenubar } = await import("./lib/menubar-installer.js");
572
+ const result = await installMenubar({
573
+ dryRun: parsed.dryRun,
574
+ ensureHook: true,
575
+ });
576
+ if (parsed.dryRun) {
577
+ console.log(`[temet] dry-run menubar install path: ${result.appPath}`);
578
+ console.log(`[temet] dry-run menubar config path: ${result.configPath}`);
579
+ return;
580
+ }
581
+ console.log(`[temet] Temet installed: ${result.appPath}`);
582
+ if (result.hookInstalled) {
583
+ console.log("[temet] Tracking enabled via SessionEnd hook.");
584
+ }
585
+ console.log("[temet] Temet opened.");
586
+ return;
587
+ }
588
+ if (parsed.command === "uninstall-menubar") {
589
+ const { uninstallMenubar } = await import("./lib/menubar-installer.js");
590
+ const result = await uninstallMenubar({ dryRun: parsed.dryRun });
591
+ if (parsed.dryRun) {
592
+ console.log(`[temet] dry-run menubar uninstall path: ${result.appPath}`);
593
+ return;
594
+ }
595
+ console.log("[temet] Temet removed.");
596
+ console.log("[temet] Tracking remains active — run temet uninstall-hook to disable it.");
597
+ return;
598
+ }
551
599
  if (parsed.command === "install-handler") {
552
600
  await installProtocolHandler(parsed.dryRun);
553
601
  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 {
@@ -3,6 +3,7 @@ export type AuditCliOptions = {
3
3
  path: string;
4
4
  track: boolean;
5
5
  narrate: boolean;
6
+ openReport: boolean;
6
7
  json: boolean;
7
8
  quiet: boolean;
8
9
  notify: boolean;
@@ -14,6 +14,8 @@ export function parseFlagBag(args) {
14
14
  if (arg === "--dry-run" ||
15
15
  arg === "--track" ||
16
16
  arg === "--narrate" ||
17
+ arg === "--no-narrate" ||
18
+ arg === "--open-report" ||
17
19
  arg === "--json" ||
18
20
  arg === "--quiet" ||
19
21
  arg === "--notify" ||
@@ -41,12 +43,20 @@ export function readOptionalString(flags, key) {
41
43
  }
42
44
  export function buildAuditCliOptions(flags, env) {
43
45
  const pathVal = readOptionalString(flags, "path") ?? "";
46
+ const json = Boolean(flags.get("json"));
47
+ const quiet = Boolean(flags.get("quiet"));
48
+ const explicitOpenReport = Boolean(flags.get("open-report"));
44
49
  return {
45
50
  path: pathVal,
46
51
  track: Boolean(flags.get("track")),
47
- narrate: Boolean(flags.get("narrate")),
48
- json: Boolean(flags.get("json")),
49
- quiet: Boolean(flags.get("quiet")),
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))),
57
+ openReport: explicitOpenReport || (!json && !quiet),
58
+ json,
59
+ quiet,
50
60
  notify: Boolean(flags.get("notify")),
51
61
  publish: Boolean(flags.get("publish")),
52
62
  yes: Boolean(flags.get("yes")),
@@ -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;