@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.
- package/assets/macos/README.md +8 -0
- package/assets/macos/Temet.app.zip +0 -0
- package/dist/audit.js +160 -31
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +79 -0
- package/dist/index.js +93 -1
- 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 +106 -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 &&
|
|
@@ -403,7 +459,19 @@ export async function runAuditCommand(opts) {
|
|
|
403
459
|
}
|
|
404
460
|
resolvedPath = resolution.path;
|
|
405
461
|
}
|
|
406
|
-
|
|
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]
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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:
|
|
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 (
|
|
606
|
+
if (reportPath && !opts.json) {
|
|
521
607
|
const reportModule = await import("./lib/report-writer.js");
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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)}`);
|
package/dist/doctor.d.ts
ADDED
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
|
|
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
|
|
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;
|