@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.
- package/assets/macos/README.md +8 -0
- package/assets/macos/Temet.app.zip +0 -0
- package/dist/audit.js +147 -30
- package/dist/index.js +56 -0
- package/dist/lib/audit-tracking.js +13 -2
- package/dist/lib/cli-args.js +6 -1
- package/dist/lib/diagnostic-runner.d.ts +13 -0
- package/dist/lib/diagnostic-runner.js +95 -0
- package/dist/lib/menubar-installer.d.ts +19 -0
- package/dist/lib/menubar-installer.js +99 -0
- package/dist/lib/menubar-state.d.ts +78 -0
- package/dist/lib/menubar-state.js +275 -0
- package/dist/lib/notifier.d.ts +2 -0
- package/dist/lib/notifier.js +11 -0
- package/dist/lib/report-writer.d.ts +2 -0
- package/dist/lib/report-writer.js +19 -1
- package/dist/plan.d.ts +33 -0
- package/dist/plan.js +90 -0
- package/dist/traces.d.ts +41 -0
- package/dist/traces.js +119 -0
- package/package.json +3 -2
|
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 {
|
|
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
|
|
279
|
-
console.log(` ${DIM}temet
|
|
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}
|
|
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
|
-
|
|
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.
|
|
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
|
|
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, "
|
|
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]
|
|
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]
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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:
|
|
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 (
|
|
594
|
+
if (reportPath && !opts.json) {
|
|
521
595
|
const reportModule = await import("./lib/report-writer.js");
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
|
30
|
-
|
|
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 {
|
package/dist/lib/cli-args.js
CHANGED
|
@@ -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
|
+
}>;
|