@temet/cli 0.3.1 → 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.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 &&
@@ -414,11 +470,11 @@ export async function runAuditCommand(opts) {
414
470
  printBanner();
415
471
  }
416
472
  if (!opts.quiet) {
417
- console.error(`[temet] scanning ${sessionFiles.length} session file(s)...`);
473
+ console.error(`[temet] reading ${sessionFiles.length} saved session file(s)...`);
418
474
  }
419
475
  let completedFiles = 0;
420
476
  if (!opts.quiet) {
421
- writeStepProgress(2, totalSteps, "Scanning sessions", 0, sessionFiles.length);
477
+ writeStepProgress(2, totalSteps, "Reading your sessions", 0, sessionFiles.length);
422
478
  }
423
479
  let scanDoneWritten = false;
424
480
  const result = await runAudit(sessionFiles, (event) => {
@@ -426,19 +482,19 @@ export async function runAuditCommand(opts) {
426
482
  return;
427
483
  if (event.phase === "scan") {
428
484
  completedFiles += 1;
429
- writeStepProgress(2, 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);
430
486
  return;
431
487
  }
432
488
  if (!scanDoneWritten) {
433
489
  scanDoneWritten = true;
434
- writeStepDone(2, totalSteps, "Scanning sessions", `${sessionFiles.length} files`);
490
+ writeStepDone(2, totalSteps, "Reading your sessions", `${sessionFiles.length} files`);
435
491
  }
436
492
  if (event.phase === "signals") {
437
493
  writeStepDone(3, totalSteps, "Extracting signals", `${formatCount(event.current ?? 0)} signals`, event.elapsedMs);
438
494
  return;
439
495
  }
440
496
  if (event.phase === "patterns") {
441
- writeStepDone(4, totalSteps, "Detecting patterns", `${formatCount(event.current ?? 0)} workflows`, event.elapsedMs);
497
+ writeStepDone(4, totalSteps, "Finding repeated patterns", `${formatCount(event.current ?? 0)} workflows`, event.elapsedMs);
442
498
  }
443
499
  });
444
500
  let { competencies } = result;
@@ -452,7 +508,7 @@ export async function runAuditCommand(opts) {
452
508
  printWarningBox("--narrate requires model access. Set ANTHROPIC_API_KEY or CLAUDE_AUTH_TOKEN.");
453
509
  }
454
510
  else {
455
- const narrated = await withSpinner(5, 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 }));
456
512
  competencies = narrated.competencies;
457
513
  bilan = narrated.bilan || undefined;
458
514
  }
@@ -463,13 +519,31 @@ export async function runAuditCommand(opts) {
463
519
  }
464
520
  if (opts.track) {
465
521
  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
- }
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(() => { });
473
547
  }
474
548
  }
475
549
  // Publish (runs even in quiet mode — it's a side-effect, not output)
@@ -506,7 +580,7 @@ export async function runAuditCommand(opts) {
506
580
  }
507
581
  }
508
582
  }
509
- // Quiet mode: no output
583
+ // Quiet mode: keep side-effects only
510
584
  if (opts.quiet)
511
585
  return;
512
586
  // Output
@@ -517,13 +591,56 @@ export async function runAuditCommand(opts) {
517
591
  else {
518
592
  printPretty(result, competencies, bilan, tracking);
519
593
  }
520
- if (opts.openReport && !opts.json) {
594
+ if (reportPath && !opts.json) {
521
595
  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)}`);
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
+ }
527
644
  }
528
645
  if (!opts.json) {
529
646
  console.error(`${ok("done", process.stderr)} ${dim(`in ${formatMs(Date.now() - commandStartedAt)}`, process.stderr)}`);
package/dist/index.js CHANGED
@@ -15,8 +15,12 @@ const HELP = `Temet CLI — discover the skills you already demonstrate in AI wo
15
15
 
16
16
  Commands:
17
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
18
20
  \ttemet install-hook Auto-audit after every Claude Code session
19
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
20
24
  \ttemet connect --address <hex> --token <t> Connect your MCP client to your Temet card
21
25
  \ttemet install-handler Register temet:// protocol handler
22
26
 
@@ -32,16 +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
39
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
40
47
  \ttemet audit --path ~/.claude/projects/my-project
41
48
  \ttemet audit --path ~/.claude/projects/my-project --track
42
49
  \ttemet audit --path ~/.claude/projects/my-project --open-report
43
50
  \ttemet audit --path ~/.claude/projects/my-project --json
44
51
  \ttemet install-hook Background audit on session end
52
+ \ttemet install-menubar Install and open Temet on macOS
45
53
  `;
46
54
  function printHelp(exitCode = 0) {
47
55
  console.log(HELP);
@@ -151,6 +159,12 @@ function parseArgs(argv) {
151
159
  options: buildAuditCliOptions(flags, process.env),
152
160
  };
153
161
  }
162
+ if (command === "traces" || command === "plan") {
163
+ return {
164
+ command,
165
+ options: buildAuditCliOptions(flags, process.env),
166
+ };
167
+ }
154
168
  if (command === "connect") {
155
169
  return {
156
170
  command,
@@ -169,6 +183,9 @@ function parseArgs(argv) {
169
183
  if (command === "install-hook" || command === "uninstall-hook") {
170
184
  return { command, dryRun };
171
185
  }
186
+ if (command === "install-menubar" || command === "uninstall-menubar") {
187
+ return { command, dryRun };
188
+ }
172
189
  console.error(`Unknown command: ${command}`);
173
190
  printHelp(1);
174
191
  }
@@ -494,6 +511,16 @@ async function run() {
494
511
  await runAuditCommand(parsed.options);
495
512
  return;
496
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
+ }
497
524
  if (parsed.command === "connect" || parsed.command === "connect-url") {
498
525
  await runWriteFlow(parsed.command, parsed.options);
499
526
  return;
@@ -540,6 +567,35 @@ async function run() {
540
567
  console.log("[temet] SessionEnd hook removed.");
541
568
  return;
542
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
+ }
543
599
  if (parsed.command === "install-handler") {
544
600
  await installProtocolHandler(parsed.dryRun);
545
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 {
@@ -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;
@@ -0,0 +1,95 @@
1
+ import { basename } from "node:path";
2
+ import { createInterface } from "node:readline";
3
+ import { resolveSessionPath } from "./path-resolver.js";
4
+ import { trackAuditSnapshot } from "./audit-tracking.js";
5
+ import { findSessionFiles, runAudit, } from "./session-audit.js";
6
+ async function chooseSessionCandidate(candidates) {
7
+ if (!process.stdin.isTTY || candidates.length === 0)
8
+ return null;
9
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
10
+ process.stderr.write(`\n[temet] I found ${candidates.length} Claude Code projects with session history. Pick one to inspect.\n\n`);
11
+ candidates.slice(0, 9).forEach((candidate, index) => {
12
+ process.stderr.write(` ${index + 1}. ${candidate.label}${index === 0 ? " (most recent)" : ""}\n`);
13
+ });
14
+ process.stderr.write("\n");
15
+ return new Promise((resolve) => {
16
+ rl.question("[temet] Choose 1-9, or press Enter for the most recent project: ", (answer) => {
17
+ rl.close();
18
+ const trimmed = answer.trim();
19
+ if (!trimmed) {
20
+ resolve(candidates[0] ?? null);
21
+ return;
22
+ }
23
+ const index = Number.parseInt(trimmed, 10) - 1;
24
+ if (!Number.isFinite(index) ||
25
+ index < 0 ||
26
+ index >= candidates.length) {
27
+ resolve(candidates[0] ?? null);
28
+ return;
29
+ }
30
+ resolve(candidates[index] ?? null);
31
+ });
32
+ });
33
+ }
34
+ export async function resolveDiagnostics(opts) {
35
+ const resolution = resolveSessionPath(opts.path || undefined, process.env);
36
+ if (!resolution.path) {
37
+ throw new Error("No Claude Code sessions found here. Run this command from a project folder, or use --path <session-dir>.");
38
+ }
39
+ let resolvedPath = resolution.path;
40
+ if (!opts.path &&
41
+ !opts.json &&
42
+ !opts.quiet &&
43
+ resolution.source === "recent" &&
44
+ resolution.candidates.length > 1) {
45
+ const selected = await chooseSessionCandidate(resolution.candidates);
46
+ resolvedPath = selected?.sessionDir ?? resolution.path;
47
+ const label = selected?.label ?? resolution.candidates[0]?.label;
48
+ if (label) {
49
+ process.stderr.write(`[temet] using ${label}\n`);
50
+ }
51
+ }
52
+ else {
53
+ const label = resolution.candidates[0]?.label;
54
+ if (label && resolution.source !== "explicit" && !opts.quiet) {
55
+ process.stderr.write(`[temet] using ${label}\n`);
56
+ }
57
+ }
58
+ const sessionFiles = findSessionFiles(resolvedPath);
59
+ if (sessionFiles.length === 0) {
60
+ throw new Error(`No .jsonl session files found in ${resolvedPath}.`);
61
+ }
62
+ if (!opts.quiet) {
63
+ process.stderr.write(`[temet] reading ${sessionFiles.length} saved session file(s)...\n`);
64
+ }
65
+ const result = await runAudit(sessionFiles);
66
+ let competencies = result.competencies;
67
+ let bilan;
68
+ if (opts.narrate && !opts.quiet) {
69
+ try {
70
+ const narratorModule = await import("./narrator-lite.js");
71
+ if (narratorModule.resolveAuth()) {
72
+ const narrated = await narratorModule.narrateCompetencies(competencies, result.combined, result.workflows, { model: opts.model || undefined });
73
+ competencies = narrated.competencies;
74
+ bilan = narrated.bilan || undefined;
75
+ }
76
+ }
77
+ catch {
78
+ // Keep deterministic fallback if narration fails.
79
+ }
80
+ }
81
+ let tracking;
82
+ if (opts.track) {
83
+ tracking = await trackAuditSnapshot(resolvedPath, result, competencies);
84
+ }
85
+ return {
86
+ resolvedPath,
87
+ result,
88
+ competencies,
89
+ bilan,
90
+ tracking,
91
+ };
92
+ }
93
+ export function defaultProjectLabel(resolvedPath) {
94
+ return basename(resolvedPath);
95
+ }
@@ -0,0 +1,19 @@
1
+ export declare const TEMET_BAR_APP_NAME = "Temet.app";
2
+ export type MenubarInstallResult = {
3
+ appPath: string;
4
+ configPath: string;
5
+ hookInstalled: boolean;
6
+ };
7
+ export declare function getMenubarAppPath(): string;
8
+ export declare function isMenubarInstalled(): boolean;
9
+ export declare function getMenubarAssetPath(): string;
10
+ export declare function installMenubar(options?: {
11
+ dryRun?: boolean;
12
+ ensureHook?: boolean;
13
+ }): Promise<MenubarInstallResult>;
14
+ export declare function uninstallMenubar(options?: {
15
+ dryRun?: boolean;
16
+ }): Promise<{
17
+ appPath: string;
18
+ configPath: string;
19
+ }>;