@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 +3 -1
- package/dist/audit.js +82 -11
- package/dist/index.js +4 -12
- package/dist/lib/cli-args.d.ts +1 -0
- package/dist/lib/cli-args.js +7 -2
- package/dist/lib/editorial-taxonomy.d.ts +17 -0
- package/dist/lib/editorial-taxonomy.js +91 -0
- package/dist/lib/path-resolver.d.ts +22 -1
- package/dist/lib/path-resolver.js +120 -24
- package/dist/lib/profile-report.d.ts +18 -0
- package/dist/lib/profile-report.js +148 -0
- package/dist/lib/report-writer.d.ts +7 -0
- package/dist/lib/report-writer.js +73 -0
- package/dist/lib/session-audit.d.ts +1 -0
- package/dist/lib/session-audit.js +2 -0
- package/package.json +1 -1
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
|
|
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 ${
|
|
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(
|
|
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(
|
|
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(
|
|
434
|
+
writeStepDone(2, totalSteps, "Scanning sessions", `${sessionFiles.length} files`);
|
|
372
435
|
}
|
|
373
436
|
if (event.phase === "signals") {
|
|
374
|
-
writeStepDone(
|
|
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(
|
|
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(
|
|
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(
|
|
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") {
|
package/dist/lib/cli-args.d.ts
CHANGED
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 === "--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
|
-
|
|
49
|
-
|
|
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):
|
|
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(
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
}
|
|
@@ -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,
|