@temet/cli 0.3.0 → 0.3.1

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/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
@@ -4,11 +4,13 @@
4
4
  import { basename } from "node:path";
5
5
  import { clearLine, createInterface, cursorTo } from "node:readline";
6
6
  import { findSessionFiles, runAudit, } from "./lib/session-audit.js";
7
+ import { resolveSessionPath, } from "./lib/path-resolver.js";
7
8
  import { trackAuditSnapshot, } from "./lib/audit-tracking.js";
8
9
  export function buildAuditJsonOutput(result, competencies, bilan, tracking) {
9
10
  return {
10
11
  sessions: result.sessionCount,
11
12
  messages: result.messageCount,
13
+ prompts: result.promptCount,
12
14
  toolCalls: result.toolCallCount,
13
15
  competencies,
14
16
  workflows: result.workflows,
@@ -297,6 +299,34 @@ async function confirmPublish(competencyCount, address) {
297
299
  });
298
300
  });
299
301
  }
302
+ async function chooseSessionCandidate(candidates) {
303
+ if (!process.stdin.isTTY || candidates.length === 0)
304
+ return null;
305
+ 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`);
307
+ candidates.slice(0, 9).forEach((candidate, index) => {
308
+ process.stderr.write(` ${index + 1}. ${candidate.label}${index === 0 ? " (most recent)" : ""}\n`);
309
+ });
310
+ process.stderr.write("\n");
311
+ return new Promise((resolve) => {
312
+ rl.question("[temet] Choose a project [1-9] or press Enter for the most recent: ", (answer) => {
313
+ rl.close();
314
+ const trimmed = answer.trim();
315
+ if (!trimmed) {
316
+ resolve(candidates[0] ?? null);
317
+ return;
318
+ }
319
+ const index = Number.parseInt(trimmed, 10) - 1;
320
+ if (!Number.isFinite(index) ||
321
+ index < 0 ||
322
+ index >= candidates.length) {
323
+ resolve(candidates[0] ?? null);
324
+ return;
325
+ }
326
+ resolve(candidates[index] ?? null);
327
+ });
328
+ });
329
+ }
300
330
  export function resolvePublishMode(yes, isTTY) {
301
331
  if (yes)
302
332
  return "skip";
@@ -338,10 +368,45 @@ async function publishCompetencies(competencies, opts) {
338
368
  }
339
369
  // ---------- Main ----------
340
370
  export async function runAuditCommand(opts) {
341
- const sessionFiles = findSessionFiles(opts.path);
371
+ const totalSteps = 4 + (opts.narrate ? 1 : 0) + (opts.publish ? 1 : 0);
372
+ const commandStartedAt = Date.now();
373
+ let resolvedPath = opts.path;
374
+ if (!opts.quiet) {
375
+ const resolution = await withSpinner(1, totalSteps, "Discovering sessions", async () => resolveSessionPath(opts.path || undefined, process.env));
376
+ if (!resolution.path) {
377
+ console.error("[temet] no Claude sessions found. Run this command from a project folder, or use --path <session-dir>.");
378
+ process.exit(1);
379
+ }
380
+ if (!opts.path &&
381
+ !opts.json &&
382
+ resolution.source === "recent" &&
383
+ resolution.candidates.length > 1) {
384
+ const selected = await chooseSessionCandidate(resolution.candidates);
385
+ resolvedPath = selected?.sessionDir ?? resolution.path;
386
+ const label = selected?.label ?? resolution.candidates[0]?.label;
387
+ if (label) {
388
+ process.stderr.write(`${dim(`[temet] using ${label}`, process.stderr)}\n`);
389
+ }
390
+ }
391
+ else {
392
+ resolvedPath = resolution.path;
393
+ const label = resolution.candidates[0]?.label;
394
+ if (label && resolution.source !== "explicit") {
395
+ process.stderr.write(`${dim(`[temet] using ${label}`, process.stderr)}\n`);
396
+ }
397
+ }
398
+ }
399
+ else {
400
+ const resolution = resolveSessionPath(opts.path || undefined, process.env);
401
+ if (!resolution.path) {
402
+ process.exit(1);
403
+ }
404
+ resolvedPath = resolution.path;
405
+ }
406
+ const sessionFiles = findSessionFiles(resolvedPath);
342
407
  if (sessionFiles.length === 0) {
343
408
  if (!opts.quiet) {
344
- console.error(`[temet] no .jsonl session files found in ${opts.path}`);
409
+ console.error(`[temet] no .jsonl session files found in ${resolvedPath}`);
345
410
  }
346
411
  process.exit(1);
347
412
  }
@@ -351,11 +416,9 @@ export async function runAuditCommand(opts) {
351
416
  if (!opts.quiet) {
352
417
  console.error(`[temet] scanning ${sessionFiles.length} session file(s)...`);
353
418
  }
354
- const totalSteps = 3 + (opts.narrate ? 1 : 0) + (opts.publish ? 1 : 0);
355
419
  let completedFiles = 0;
356
- const commandStartedAt = Date.now();
357
420
  if (!opts.quiet) {
358
- writeStepProgress(1, totalSteps, "Scanning sessions", 0, sessionFiles.length);
421
+ writeStepProgress(2, totalSteps, "Scanning sessions", 0, sessionFiles.length);
359
422
  }
360
423
  let scanDoneWritten = false;
361
424
  const result = await runAudit(sessionFiles, (event) => {
@@ -363,19 +426,19 @@ export async function runAuditCommand(opts) {
363
426
  return;
364
427
  if (event.phase === "scan") {
365
428
  completedFiles += 1;
366
- writeStepProgress(1, totalSteps, "Scanning sessions", completedFiles, sessionFiles.length, event.file ? basename(event.file) : undefined);
429
+ writeStepProgress(2, totalSteps, "Scanning sessions", completedFiles, sessionFiles.length, event.file ? basename(event.file) : undefined);
367
430
  return;
368
431
  }
369
432
  if (!scanDoneWritten) {
370
433
  scanDoneWritten = true;
371
- writeStepDone(1, totalSteps, "Scanning sessions", `${sessionFiles.length} files`);
434
+ writeStepDone(2, totalSteps, "Scanning sessions", `${sessionFiles.length} files`);
372
435
  }
373
436
  if (event.phase === "signals") {
374
- writeStepDone(2, totalSteps, "Extracting signals", `${formatCount(event.current ?? 0)} signals`, event.elapsedMs);
437
+ writeStepDone(3, totalSteps, "Extracting signals", `${formatCount(event.current ?? 0)} signals`, event.elapsedMs);
375
438
  return;
376
439
  }
377
440
  if (event.phase === "patterns") {
378
- writeStepDone(3, totalSteps, "Detecting patterns", `${formatCount(event.current ?? 0)} workflows`, event.elapsedMs);
441
+ writeStepDone(4, totalSteps, "Detecting patterns", `${formatCount(event.current ?? 0)} workflows`, event.elapsedMs);
379
442
  }
380
443
  });
381
444
  let { competencies } = result;
@@ -389,7 +452,7 @@ export async function runAuditCommand(opts) {
389
452
  printWarningBox("--narrate requires model access. Set ANTHROPIC_API_KEY or CLAUDE_AUTH_TOKEN.");
390
453
  }
391
454
  else {
392
- const narrated = await withSpinner(4, totalSteps, "Narrating profile", async () => narratorModule.narrateCompetencies(competencies, result.combined, result.workflows, { model: opts.model || undefined }));
455
+ const narrated = await withSpinner(5, totalSteps, "Narrating profile", async () => narratorModule.narrateCompetencies(competencies, result.combined, result.workflows, { model: opts.model || undefined }));
393
456
  competencies = narrated.competencies;
394
457
  bilan = narrated.bilan || undefined;
395
458
  }
@@ -399,7 +462,7 @@ export async function runAuditCommand(opts) {
399
462
  }
400
463
  }
401
464
  if (opts.track) {
402
- tracking = await trackAuditSnapshot(opts.path, result, competencies);
465
+ tracking = await trackAuditSnapshot(resolvedPath, result, competencies);
403
466
  // OS notification (only behind --notify, and only if something changed)
404
467
  if (opts.notify && !tracking.skipped && tracking.changes.length > 0) {
405
468
  const { formatNotification, sendNotification } = await import("./lib/notifier.js");
@@ -454,6 +517,14 @@ export async function runAuditCommand(opts) {
454
517
  else {
455
518
  printPretty(result, competencies, bilan, tracking);
456
519
  }
520
+ if (opts.openReport && !opts.json) {
521
+ 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)}`);
527
+ }
457
528
  if (!opts.json) {
458
529
  console.error(`${ok("done", process.stderr)} ${dim(`in ${formatMs(Date.now() - commandStartedAt)}`, process.stderr)}`);
459
530
  }
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";
@@ -24,6 +23,7 @@ Commands:
24
23
  Audit options:
25
24
  \t--path <dir> Directory containing .jsonl session files (auto-detected if omitted)
26
25
  \t--track Save a local snapshot and compare against the previous audit
26
+ \t--open-report Explicitly save and open the text report (opened by default unless --json or --quiet)
27
27
  \t--json Output structured JSON instead of terminal display
28
28
  \t--quiet Suppress all output (for background hooks)
29
29
  \t--notify Send an OS notification on skill changes (used with --track)
@@ -36,8 +36,10 @@ Advanced:
36
36
 
37
37
  Examples:
38
38
  \ttemet audit Auto-detect sessions from cwd
39
+ \ttemet audit Open the text report by default
39
40
  \ttemet audit --path ~/.claude/projects/my-project
40
41
  \ttemet audit --path ~/.claude/projects/my-project --track
42
+ \ttemet audit --path ~/.claude/projects/my-project --open-report
41
43
  \ttemet audit --path ~/.claude/projects/my-project --json
42
44
  \ttemet install-hook Background audit on session end
43
45
  `;
@@ -144,19 +146,9 @@ function parseArgs(argv) {
144
146
  const { flags, positionals } = parseFlagBag(rest);
145
147
  const dryRun = Boolean(flags.get("dry-run"));
146
148
  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
149
  return {
158
150
  command,
159
- options,
151
+ options: buildAuditCliOptions(flags, process.env),
160
152
  };
161
153
  }
162
154
  if (command === "connect") {
@@ -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,7 @@ export function parseFlagBag(args) {
14
14
  if (arg === "--dry-run" ||
15
15
  arg === "--track" ||
16
16
  arg === "--narrate" ||
17
+ arg === "--open-report" ||
17
18
  arg === "--json" ||
18
19
  arg === "--quiet" ||
19
20
  arg === "--notify" ||
@@ -41,12 +42,16 @@ export function readOptionalString(flags, key) {
41
42
  }
42
43
  export function buildAuditCliOptions(flags, env) {
43
44
  const pathVal = readOptionalString(flags, "path") ?? "";
45
+ const json = Boolean(flags.get("json"));
46
+ const quiet = Boolean(flags.get("quiet"));
47
+ const explicitOpenReport = Boolean(flags.get("open-report"));
44
48
  return {
45
49
  path: pathVal,
46
50
  track: Boolean(flags.get("track")),
47
51
  narrate: Boolean(flags.get("narrate")),
48
- json: Boolean(flags.get("json")),
49
- quiet: Boolean(flags.get("quiet")),
52
+ openReport: explicitOpenReport || (!json && !quiet),
53
+ json,
54
+ quiet,
50
55
  notify: Boolean(flags.get("notify")),
51
56
  publish: Boolean(flags.get("publish")),
52
57
  yes: Boolean(flags.get("yes")),
@@ -0,0 +1,17 @@
1
+ import type { CompetencyEntry } from "./types.js";
2
+ type EditorialEvidence = {
3
+ examples: string[];
4
+ decisionCriteria: string[];
5
+ antiPatterns: string[];
6
+ };
7
+ type EditorialSkillPreset = {
8
+ title: string;
9
+ tagline?: string;
10
+ match: RegExp[];
11
+ render: (entry: CompetencyEntry, evidence: EditorialEvidence) => string;
12
+ };
13
+ export declare function extractEditorialEvidence(entry: CompetencyEntry): EditorialEvidence;
14
+ export declare function isCommoditySkillName(name: string): boolean;
15
+ export declare function fallbackEditorialSummary(entry: CompetencyEntry): string;
16
+ export declare function matchEditorialSkill(entry: CompetencyEntry): EditorialSkillPreset | null;
17
+ export {};
@@ -0,0 +1,91 @@
1
+ const COMMODITY_PATTERNS = [
2
+ /\btypescript\b/i,
3
+ /\bjavascript\b/i,
4
+ /\bnext\.?js\b/i,
5
+ /\breact\b/i,
6
+ /\bapi routes?\b/i,
7
+ /\bshell scripting\b/i,
8
+ /\bcommand-line\b/i,
9
+ /\bterminal fluency\b/i,
10
+ /\bcodebase navigation\b/i,
11
+ ];
12
+ function firstOrFallback(values, fallback) {
13
+ return values.find(Boolean) ?? fallback;
14
+ }
15
+ export function extractEditorialEvidence(entry) {
16
+ return {
17
+ examples: entry.evidence.examples.slice(0, 2),
18
+ decisionCriteria: entry.evidence.decisionCriteria.slice(0, 2),
19
+ antiPatterns: entry.evidence.antiPatterns.slice(0, 2),
20
+ };
21
+ }
22
+ export function isCommoditySkillName(name) {
23
+ return COMMODITY_PATTERNS.some((pattern) => pattern.test(name));
24
+ }
25
+ export function fallbackEditorialSummary(entry) {
26
+ const evidence = extractEditorialEvidence(entry);
27
+ const example = firstOrFallback(evidence.examples, `The signal behind ${entry.name} appears repeatedly in your sessions.`);
28
+ const decision = evidence.decisionCriteria[0]
29
+ ? `You also make decisions that reinforce it: ${evidence.decisionCriteria[0]}`
30
+ : "It is not just a topic you touch once. It shows up as a repeated working habit.";
31
+ return `${example} ${decision}`;
32
+ }
33
+ const EDITORIAL_SKILL_PRESETS = [
34
+ {
35
+ title: "Plan-First Discipline",
36
+ tagline: "You force structure before execution",
37
+ match: [/spec-driven/i, /implementation planning/i],
38
+ render: (_entry, evidence) => `${firstOrFallback(evidence.examples, "You repeatedly create structure before touching implementation.")} ${firstOrFallback(evidence.decisionCriteria, "You work as if planning, execution, and documentation should stay tied together, not happen in separate worlds.")}`,
39
+ },
40
+ {
41
+ title: "Codebase Archaeology",
42
+ tagline: "You read systems before changing them",
43
+ match: [/deep code comprehension/i, /before change/i],
44
+ render: (_entry, evidence) => `${firstOrFallback(evidence.examples, "You spend time reading the existing system before making changes.")} ${firstOrFallback(evidence.antiPatterns, "You do not treat the codebase like a blank canvas. You reconstruct its history and constraints first.")}`,
45
+ },
46
+ {
47
+ title: "Precision Refactoring",
48
+ tagline: "You change moving systems without losing the thread",
49
+ match: [/precise refactoring/i, /complex dependencies/i],
50
+ render: (_entry, evidence) => `${firstOrFallback(evidence.examples, "You refactor with unusually tight control over moving parts.")} ${firstOrFallback(evidence.decisionCriteria, "The signal is less about rewriting fast and more about changing a system without breaking its shape.")}`,
51
+ },
52
+ {
53
+ title: "Feedback-Loop Delivery",
54
+ tagline: "You drive work through short correction cycles",
55
+ match: [/feedback loops?/i, /test-driven delivery/i],
56
+ render: (_entry, evidence) => `${firstOrFallback(evidence.examples, "You move through short loops of change, test, and correction instead of one large pass.")} ${firstOrFallback(evidence.antiPatterns, "What matters here is not speed alone. It is your willingness to keep the loop tight until the output holds.")}`,
57
+ },
58
+ {
59
+ title: "Competitive Intelligence",
60
+ tagline: "You learn by comparing what already exists",
61
+ match: [/competitive intelligence/i, /research/i],
62
+ render: (_entry, evidence) => `${firstOrFallback(evidence.examples, "You repeatedly study adjacent tools, projects, and implementations before deciding what to adopt.")} ${firstOrFallback(evidence.decisionCriteria, "This is less trend-following than a disciplined habit of using outside reference points to sharpen your own decisions.")}`,
63
+ },
64
+ {
65
+ title: "Product Taste",
66
+ tagline: "You notice what feels right before it is fully rationalized",
67
+ match: [/product taste/i, /ux/i, /visual/i],
68
+ render: (_entry, evidence) => `${firstOrFallback(evidence.examples, "You repeatedly intervene on presentation, feel, and quality before those issues become formal design tickets.")} ${firstOrFallback(evidence.antiPatterns, "The pattern suggests judgment about what makes a product feel coherent, not just whether it technically works.")}`,
69
+ },
70
+ {
71
+ title: "Change-Risk Judgment",
72
+ tagline: "You treat risky changes as decisions, not just edits",
73
+ match: [/version control judgment/i, /risky changes/i, /security-aware/i],
74
+ render: (_entry, evidence) => `${firstOrFallback(evidence.antiPatterns, "You repeatedly catch where a change could introduce avoidable risk.")} ${firstOrFallback(evidence.decisionCriteria, "The signal here is judgment under uncertainty: knowing when a change should be slowed down, isolated, or validated more carefully.")}`,
75
+ },
76
+ {
77
+ title: "Contract Rigor",
78
+ tagline: "You care about the shape of systems at their boundaries",
79
+ match: [/contract rigor/i, /api boundary/i, /schema/i],
80
+ render: (_entry, evidence) => `${firstOrFallback(evidence.examples, "You repeatedly tighten interfaces, contracts, and the places where systems meet.")} ${firstOrFallback(evidence.decisionCriteria, "You seem to care less about local implementation cleverness than about whether the edges stay understandable and stable.")}`,
81
+ },
82
+ ];
83
+ export function matchEditorialSkill(entry) {
84
+ const haystack = `${entry.name} ${entry.description}`.toLowerCase();
85
+ for (const preset of EDITORIAL_SKILL_PRESETS) {
86
+ if (preset.match.some((pattern) => pattern.test(haystack))) {
87
+ return preset;
88
+ }
89
+ }
90
+ return null;
91
+ }
@@ -1,5 +1,24 @@
1
+ export type SessionCandidate = {
2
+ sessionDir: string;
3
+ label: string;
4
+ updatedAtMs: number;
5
+ };
6
+ export type SessionResolution = {
7
+ path: string | null;
8
+ source: "explicit" | "env" | "stdin" | "cwd" | "recent" | "none";
9
+ candidates: SessionCandidate[];
10
+ };
11
+ type ResolveSessionPathOptions = {
12
+ cwd?: string;
13
+ claudeRoot?: string;
14
+ hookInput?: {
15
+ cwd?: string;
16
+ } | null;
17
+ };
1
18
  /** Convert an absolute project directory to the Claude Code session directory slug. */
2
19
  export declare function cwdToSessionDir(cwd: string): string;
20
+ export declare function humanizeSessionDir(sessionDir: string): string;
21
+ export declare function findClaudeProjectCandidates(claudeRoot?: string): SessionCandidate[];
3
22
  /**
4
23
  * Try to read the JSON stdin from a Claude Code hook (non-blocking).
5
24
  * Only safe to call when we know stdin has finite piped data (hook context).
@@ -14,5 +33,7 @@ export declare function readHookStdin(): {
14
33
  * 2. $CLAUDE_PROJECT_DIR env var
15
34
  * 3. stdin JSON cwd (hook context)
16
35
  * 4. process.cwd()
36
+ * 5. most recent valid Claude project
17
37
  */
18
- export declare function resolveSessionPath(explicit: string | undefined, env: NodeJS.ProcessEnv): string | null;
38
+ export declare function resolveSessionPath(explicit: string | undefined, env: NodeJS.ProcessEnv, options?: ResolveSessionPathOptions): SessionResolution;
39
+ export {};
@@ -1,10 +1,75 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
+ const DEFAULT_CLAUDE_ROOT = path.join(homedir(), ".claude", "projects");
4
5
  /** Convert an absolute project directory to the Claude Code session directory slug. */
5
6
  export function cwdToSessionDir(cwd) {
6
7
  const slug = path.resolve(cwd).replace(/\//g, "-");
7
- return path.join(homedir(), ".claude", "projects", slug);
8
+ return path.join(DEFAULT_CLAUDE_ROOT, slug);
9
+ }
10
+ export function humanizeSessionDir(sessionDir) {
11
+ const slug = path.basename(sessionDir);
12
+ if (!slug)
13
+ return sessionDir;
14
+ if (slug.startsWith("-")) {
15
+ return slug.replace(/-/g, "/");
16
+ }
17
+ return slug;
18
+ }
19
+ function hasJsonlSessions(dir) {
20
+ try {
21
+ for (const entry of readdirSync(dir)) {
22
+ const full = path.join(dir, entry);
23
+ const stat = statSync(full);
24
+ if (stat.isDirectory()) {
25
+ if (hasJsonlSessions(full))
26
+ return true;
27
+ continue;
28
+ }
29
+ if (stat.isFile() && entry.endsWith(".jsonl"))
30
+ return true;
31
+ }
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ return false;
37
+ }
38
+ function candidateFromSessionDir(sessionDir) {
39
+ if (!existsSync(sessionDir) || !hasJsonlSessions(sessionDir))
40
+ return null;
41
+ let updatedAtMs = 0;
42
+ try {
43
+ updatedAtMs = statSync(sessionDir).mtimeMs;
44
+ }
45
+ catch {
46
+ updatedAtMs = 0;
47
+ }
48
+ return {
49
+ sessionDir,
50
+ label: humanizeSessionDir(sessionDir),
51
+ updatedAtMs,
52
+ };
53
+ }
54
+ export function findClaudeProjectCandidates(claudeRoot = DEFAULT_CLAUDE_ROOT) {
55
+ try {
56
+ return readdirSync(claudeRoot)
57
+ .map((entry) => path.join(claudeRoot, entry))
58
+ .filter((full) => {
59
+ try {
60
+ return statSync(full).isDirectory();
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ })
66
+ .map(candidateFromSessionDir)
67
+ .filter((candidate) => candidate !== null)
68
+ .sort((a, b) => b.updatedAtMs - a.updatedAtMs);
69
+ }
70
+ catch {
71
+ return [];
72
+ }
8
73
  }
9
74
  /**
10
75
  * Try to read the JSON stdin from a Claude Code hook (non-blocking).
@@ -13,8 +78,6 @@ export function cwdToSessionDir(cwd) {
13
78
  */
14
79
  export function readHookStdin() {
15
80
  try {
16
- // Only attempt if stdin is piped (non-TTY) AND we're in a hook context.
17
- // CLAUDECODE env var is set when running inside Claude Code.
18
81
  if (process.stdin.isTTY)
19
82
  return null;
20
83
  if (!process.env.CLAUDECODE)
@@ -32,25 +95,58 @@ export function readHookStdin() {
32
95
  * 2. $CLAUDE_PROJECT_DIR env var
33
96
  * 3. stdin JSON cwd (hook context)
34
97
  * 4. process.cwd()
98
+ * 5. most recent valid Claude project
35
99
  */
36
- export function resolveSessionPath(explicit, env) {
37
- if (explicit)
38
- return explicit;
39
- // Priority 2: CLAUDE_PROJECT_DIR env var (guaranteed in Claude Code hooks)
40
- const projectDir = env.CLAUDE_PROJECT_DIR;
41
- if (projectDir) {
42
- const d = cwdToSessionDir(projectDir);
43
- if (existsSync(d))
44
- return d;
45
- }
46
- // Priority 3: stdin JSON cwd (hook context)
47
- const hookInput = readHookStdin();
48
- if (hookInput?.cwd) {
49
- const d = cwdToSessionDir(hookInput.cwd);
50
- if (existsSync(d))
51
- return d;
52
- }
53
- // Priority 4: process.cwd()
54
- const d = cwdToSessionDir(process.cwd());
55
- return existsSync(d) ? d : null;
100
+ export function resolveSessionPath(explicit, env, options = {}) {
101
+ if (explicit) {
102
+ return {
103
+ path: explicit,
104
+ source: "explicit",
105
+ candidates: [],
106
+ };
107
+ }
108
+ const cwd = options.cwd ?? process.cwd();
109
+ const claudeRoot = options.claudeRoot ?? DEFAULT_CLAUDE_ROOT;
110
+ const fromEnv = env.CLAUDE_PROJECT_DIR
111
+ ? candidateFromSessionDir(path.join(claudeRoot, path.resolve(env.CLAUDE_PROJECT_DIR).replace(/\//g, "-")))
112
+ : null;
113
+ if (fromEnv) {
114
+ return {
115
+ path: fromEnv.sessionDir,
116
+ source: "env",
117
+ candidates: [fromEnv],
118
+ };
119
+ }
120
+ const hookInput = options.hookInput ?? readHookStdin();
121
+ const fromStdin = hookInput?.cwd
122
+ ? candidateFromSessionDir(path.join(claudeRoot, path.resolve(hookInput.cwd).replace(/\//g, "-")))
123
+ : null;
124
+ if (fromStdin) {
125
+ return {
126
+ path: fromStdin.sessionDir,
127
+ source: "stdin",
128
+ candidates: [fromStdin],
129
+ };
130
+ }
131
+ const fromCwd = candidateFromSessionDir(path.join(claudeRoot, path.resolve(cwd).replace(/\//g, "-")));
132
+ if (fromCwd) {
133
+ return {
134
+ path: fromCwd.sessionDir,
135
+ source: "cwd",
136
+ candidates: [fromCwd],
137
+ };
138
+ }
139
+ const candidates = findClaudeProjectCandidates(claudeRoot);
140
+ if (candidates.length > 0) {
141
+ return {
142
+ path: candidates[0].sessionDir,
143
+ source: "recent",
144
+ candidates,
145
+ };
146
+ }
147
+ return {
148
+ path: null,
149
+ source: "none",
150
+ candidates: [],
151
+ };
56
152
  }
@@ -0,0 +1,18 @@
1
+ import type { TrackingResult } from "./audit-tracking.js";
2
+ import type { AuditResult } from "./session-audit.js";
3
+ import type { CompetencyEntry } from "./types.js";
4
+ export type NarrativeSection = {
5
+ title: string;
6
+ tagline?: string;
7
+ body: string;
8
+ };
9
+ export type SkillAuditReport = {
10
+ title: string;
11
+ subtitle: string;
12
+ tacitSkills: NarrativeSection[];
13
+ blindSpots: NarrativeSection[];
14
+ professionalDna: string;
15
+ metaSkill: NarrativeSection;
16
+ meaningfulChanges: string[];
17
+ };
18
+ export declare function buildSkillAuditReport(result: Pick<AuditResult, "sessionCount" | "promptCount" | "toolCallCount" | "workflowCount" | "signals">, competencies: CompetencyEntry[], tracking?: Pick<TrackingResult, "changes">): SkillAuditReport;
@@ -0,0 +1,148 @@
1
+ import { extractEditorialEvidence, fallbackEditorialSummary, isCommoditySkillName, matchEditorialSkill, } from "./editorial-taxonomy.js";
2
+ const PROFICIENCY_SCORE = {
3
+ expert: 5,
4
+ proficient: 4,
5
+ competent: 3,
6
+ advanced_beginner: 2,
7
+ novice: 1,
8
+ };
9
+ function scoreCompetency(entry) {
10
+ const evidenceCount = entry.evidence.examples.length +
11
+ entry.evidence.decisionCriteria.length +
12
+ entry.evidence.antiPatterns.length +
13
+ entry.evidence.mentorAdvice.length;
14
+ return PROFICIENCY_SCORE[entry.proficiencyLevel] * 10 + evidenceCount;
15
+ }
16
+ function pickTacitSkills(competencies) {
17
+ const sorted = [...competencies].sort((a, b) => scoreCompetency(b) - scoreCompetency(a));
18
+ const usedTitles = new Set();
19
+ const sections = [];
20
+ const presetMatches = sorted.filter((entry) => matchEditorialSkill(entry));
21
+ const fallbackCandidates = sorted.filter((entry) => !matchEditorialSkill(entry) && !isCommoditySkillName(entry.name));
22
+ for (const entry of presetMatches) {
23
+ const preset = matchEditorialSkill(entry);
24
+ const title = preset?.title ?? entry.name;
25
+ if (usedTitles.has(title))
26
+ continue;
27
+ usedTitles.add(title);
28
+ const evidence = extractEditorialEvidence(entry);
29
+ sections.push({
30
+ title,
31
+ tagline: preset?.tagline,
32
+ body: preset
33
+ ? preset.render(entry, evidence)
34
+ : fallbackEditorialSummary(entry),
35
+ });
36
+ if (sections.length >= 10)
37
+ break;
38
+ }
39
+ if (sections.length < 6) {
40
+ for (const entry of fallbackCandidates) {
41
+ const preset = matchEditorialSkill(entry);
42
+ const title = preset?.title ?? entry.name;
43
+ if (usedTitles.has(title))
44
+ continue;
45
+ usedTitles.add(title);
46
+ const evidence = extractEditorialEvidence(entry);
47
+ sections.push({
48
+ title,
49
+ tagline: preset?.tagline,
50
+ body: preset
51
+ ? preset.render(entry, evidence)
52
+ : fallbackEditorialSummary(entry),
53
+ });
54
+ if (sections.length >= 8)
55
+ break;
56
+ }
57
+ }
58
+ return sections;
59
+ }
60
+ function hasSkill(competencies, pattern) {
61
+ return competencies.some((entry) => pattern.test(entry.name.toLowerCase()));
62
+ }
63
+ function buildBlindSpots(result, competencies) {
64
+ const blindSpots = [];
65
+ if (result.workflowCount >= 80) {
66
+ blindSpots.push({
67
+ title: "Feature sprawl",
68
+ body: "You can generate momentum faster than you stabilize it. When the number of patterns and directions keeps climbing, the risk is not lack of output. It is losing the discipline to decide what deserves to become a finished path.",
69
+ });
70
+ }
71
+ if (hasSkill(competencies, /version control/) ||
72
+ result.signals.some((signal) => /version control/i.test(signal.skill))) {
73
+ blindSpots.push({
74
+ title: "Git hygiene under pressure",
75
+ body: "Your work suggests strong version-control awareness, but also the kind of environment where branch, commit, and release pressure can still create avoidable mess. This is usually a systems problem, not a knowledge problem.",
76
+ });
77
+ }
78
+ if (hasSkill(competencies, /research|architecture|code comprehension|refactoring/)) {
79
+ blindSpots.push({
80
+ title: "Technical detours",
81
+ body: "You are capable of going deep, which is a strength until exploration keeps running after product value has flattened. The risk is not bad engineering. It is staying fascinated one loop too long.",
82
+ });
83
+ }
84
+ const fallback = [
85
+ {
86
+ title: "Feature sprawl",
87
+ body: "You move quickly once a direction looks promising. The risk is not inactivity. It is letting the number of live possibilities grow faster than the number of finished outcomes.",
88
+ },
89
+ {
90
+ title: "Coordination drag",
91
+ body: "As your systems become more capable, the hidden cost shifts toward alignment, naming, and decision clarity. Work can stay technically correct while still becoming harder to coordinate.",
92
+ },
93
+ {
94
+ title: "Technical detours",
95
+ body: "Deep exploration is one of your strengths, but it can keep producing value long after the product question should have ended the loop. Good curiosity still needs a stopping rule.",
96
+ },
97
+ ];
98
+ for (const item of fallback) {
99
+ if (blindSpots.some((existing) => existing.title === item.title))
100
+ continue;
101
+ blindSpots.push(item);
102
+ if (blindSpots.length >= 3)
103
+ break;
104
+ }
105
+ return blindSpots.slice(0, 3);
106
+ }
107
+ function buildProfessionalDna(tacitSkills, competencies) {
108
+ const anchors = tacitSkills.slice(0, 3).map((skill) => skill.title);
109
+ const stacks = competencies
110
+ .filter((entry) => /cloudflare|react|api|type system|component|security|shell/i.test(entry.name))
111
+ .slice(0, 4)
112
+ .map((entry) => entry.name);
113
+ const stackLine = stacks.length > 0
114
+ ? `The technical signal clusters around ${stacks.join(", ")}.`
115
+ : "The technical signal clusters around architecture, implementation judgment, and repeated execution patterns.";
116
+ return `This profile points to someone who turns ambiguity into execution. The strongest signals are ${anchors.join(", ")}, which together describe a person who reads systems carefully, imposes structure early, and keeps moving through short feedback loops instead of relying on one big heroic pass. ${stackLine} The distinctive advantage is not one isolated skill, but the ability to convert messy work into something legible, directional, and reusable.`;
117
+ }
118
+ function buildMetaSkill(tacitSkills) {
119
+ const titles = tacitSkills.map((skill) => skill.title.toLowerCase());
120
+ if (titles.some((title) => title.includes("plan-first")) &&
121
+ titles.some((title) => title.includes("codebase archaeology"))) {
122
+ return {
123
+ title: "Turning ambiguity into executable systems",
124
+ body: "Across the report, the deepest pattern is not just building or reviewing. It is repeatedly taking vague, shifting work and turning it into a system that can be understood, executed, and improved. That is the skill underneath the other skills.",
125
+ };
126
+ }
127
+ return {
128
+ title: "Building feedback loops around real work",
129
+ body: "The common thread is not any one tool or framework. It is the habit of turning real work into a loop: observe, judge, change, test, and run again. That is what makes the rest of the profile coherent.",
130
+ };
131
+ }
132
+ function buildMeaningfulChanges(tracking) {
133
+ if (!tracking || tracking.changes.length === 0)
134
+ return [];
135
+ return tracking.changes.map((change) => `${change.title} — ${change.detail}`);
136
+ }
137
+ export function buildSkillAuditReport(result, competencies, tracking) {
138
+ const tacitSkills = pickTacitSkills(competencies);
139
+ return {
140
+ title: `TACIT SKILL EXTRACTION — ${result.promptCount.toLocaleString("en-US")} prompts analyzed`,
141
+ subtitle: `${result.sessionCount.toLocaleString("en-US")} sessions · ${result.toolCallCount.toLocaleString("en-US")} tool calls · ${result.workflowCount.toLocaleString("en-US")} workflow patterns. This is a deterministic Temet reading of the skills, habits, and judgment patterns that show up repeatedly in your real AI work.`,
142
+ tacitSkills,
143
+ blindSpots: buildBlindSpots(result, competencies),
144
+ professionalDna: buildProfessionalDna(tacitSkills, competencies),
145
+ metaSkill: buildMetaSkill(tacitSkills),
146
+ meaningfulChanges: buildMeaningfulChanges(tracking),
147
+ };
148
+ }
@@ -0,0 +1,7 @@
1
+ import type { AuditResult } from "./session-audit.js";
2
+ import type { TrackingResult } from "./audit-tracking.js";
3
+ import type { CompetencyEntry } from "./types.js";
4
+ export declare function buildAuditTextReport(result: Pick<AuditResult, "sessionCount" | "promptCount" | "toolCallCount" | "signals" | "workflowCount" | "workflows">, competencies: CompetencyEntry[], bilan?: string, tracking?: Pick<TrackingResult, "changes" | "current">): string;
5
+ export declare function buildReportPath(projectLabel: string, now?: Date): string;
6
+ export declare function saveAuditTextReport(projectLabel: string, content: string, now?: Date): Promise<string>;
7
+ export declare function openReportFile(filePath: string, platform?: NodeJS.Platform): Promise<void>;
@@ -0,0 +1,73 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { buildSkillAuditReport } from "./profile-report.js";
7
+ const execFileAsync = promisify(execFile);
8
+ const TEMET_WORDMARK = [
9
+ "████████╗███████╗███╗ ███╗███████╗████████╗",
10
+ "╚══██╔══╝██╔════╝████╗ ████║██╔════╝╚══██╔══╝",
11
+ " ██║ █████╗ ██╔████╔██║█████╗ ██║ ",
12
+ " ██║ ██╔══╝ ██║╚██╔╝██║██╔══╝ ██║ ",
13
+ " ██║ ███████╗██║ ╚═╝ ██║███████╗ ██║ ",
14
+ " ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ",
15
+ ];
16
+ function divider(char = "─", width = 66) {
17
+ return char.repeat(width);
18
+ }
19
+ function section(title, body) {
20
+ return [title, divider(), "", ...body, ""];
21
+ }
22
+ export function buildAuditTextReport(result, competencies, bilan, tracking) {
23
+ const lines = [];
24
+ const profile = buildSkillAuditReport(result, competencies, tracking);
25
+ lines.push(...TEMET_WORDMARK, "", "By Temet · Encode human intelligence", "temet-skills-audit", "", profile.title, "", profile.subtitle, "");
26
+ lines.push(...section(`YOUR ${profile.tacitSkills.length} TACIT SKILLS`, profile.tacitSkills.flatMap((entry, index) => {
27
+ const header = entry.tagline
28
+ ? `${index + 1}. ${entry.title} — ${entry.tagline}`
29
+ : `${index + 1}. ${entry.title}`;
30
+ return [header, "", entry.body, ""];
31
+ })));
32
+ if (profile.meaningfulChanges.length > 0) {
33
+ lines.push(...section("WHAT CHANGED SINCE THE LAST AUDIT", profile.meaningfulChanges));
34
+ }
35
+ lines.push(...section("YOUR 3 BLIND SPOTS", profile.blindSpots.flatMap((entry, index) => [
36
+ `${index + 1}. ${entry.title}`,
37
+ "",
38
+ entry.body,
39
+ "",
40
+ ])));
41
+ lines.push(...section("YOUR PROFESSIONAL DNA", [
42
+ bilan?.trim() || profile.professionalDna,
43
+ ]));
44
+ lines.push(...section("THE META-SKILL THAT TIES IT ALL TOGETHER", [
45
+ profile.metaSkill.title,
46
+ "",
47
+ profile.metaSkill.body,
48
+ ]));
49
+ lines.push(divider(), "", "Thanks for reading.", "Arnaud");
50
+ return `${lines.join("\n").trim()}\n`;
51
+ }
52
+ export function buildReportPath(projectLabel, now = new Date()) {
53
+ const date = now.toISOString().replace(/:/g, "-");
54
+ const safeLabel = projectLabel.replace(/[^a-zA-Z0-9._-]+/g, "-");
55
+ return path.join(homedir(), ".temet", "reports", safeLabel, `temet-skills-audit-${date}.txt`);
56
+ }
57
+ export async function saveAuditTextReport(projectLabel, content, now = new Date()) {
58
+ const filePath = buildReportPath(projectLabel, now);
59
+ await mkdir(path.dirname(filePath), { recursive: true });
60
+ await writeFile(filePath, content, "utf8");
61
+ return filePath;
62
+ }
63
+ export async function openReportFile(filePath, platform = process.platform) {
64
+ if (platform === "darwin") {
65
+ await execFileAsync("open", ["-a", "TextEdit", filePath]);
66
+ return;
67
+ }
68
+ if (platform === "win32") {
69
+ await execFileAsync("notepad", [filePath]);
70
+ return;
71
+ }
72
+ await execFileAsync("xdg-open", [filePath]);
73
+ }
@@ -5,6 +5,7 @@ import type { CompetencyEntry } from "./types.js";
5
5
  export type AuditResult = {
6
6
  sessionCount: number;
7
7
  messageCount: number;
8
+ promptCount: number;
8
9
  toolCallCount: number;
9
10
  signalCount: number;
10
11
  workflowCount: number;
@@ -78,9 +78,11 @@ export async function runAudit(sessionFiles, onProgress) {
78
78
  elapsedMs: Date.now() - workflowStartedAt,
79
79
  });
80
80
  const competencies = mapSignalsToCompetencies(allSignals);
81
+ const promptCount = combined.messages.filter((message) => message.role === "user").length;
81
82
  return {
82
83
  sessionCount: sessionFiles.length,
83
84
  messageCount: combined.messages.length,
85
+ promptCount,
84
86
  toolCallCount: combined.toolCalls.length,
85
87
  signalCount: allSignals.length,
86
88
  workflowCount: workflows.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@temet/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Temet CLI — discover the skills you already demonstrate in AI work",
5
5
  "keywords": [
6
6
  "temet",