@temet/cli 0.2.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.
Files changed (38) hide show
  1. package/dist/audit.d.ts +34 -0
  2. package/dist/audit.js +531 -0
  3. package/dist/index.js +85 -40
  4. package/dist/lib/analysis-types.d.ts +7 -0
  5. package/dist/lib/analysis-types.js +1 -0
  6. package/dist/lib/audit-tracking.d.ts +55 -0
  7. package/dist/lib/audit-tracking.js +185 -0
  8. package/dist/lib/cli-args.d.ts +22 -0
  9. package/dist/lib/cli-args.js +65 -0
  10. package/dist/lib/editorial-taxonomy.d.ts +17 -0
  11. package/dist/lib/editorial-taxonomy.js +91 -0
  12. package/dist/lib/heuristics.d.ts +15 -0
  13. package/dist/lib/heuristics.js +341 -0
  14. package/dist/lib/hook-installer.d.ts +13 -0
  15. package/dist/lib/hook-installer.js +130 -0
  16. package/dist/lib/narrator-lite.d.ts +25 -0
  17. package/dist/lib/narrator-lite.js +231 -0
  18. package/dist/lib/notifier.d.ts +9 -0
  19. package/dist/lib/notifier.js +57 -0
  20. package/dist/lib/path-resolver.d.ts +39 -0
  21. package/dist/lib/path-resolver.js +152 -0
  22. package/dist/lib/profile-report.d.ts +18 -0
  23. package/dist/lib/profile-report.js +148 -0
  24. package/dist/lib/report-writer.d.ts +7 -0
  25. package/dist/lib/report-writer.js +73 -0
  26. package/dist/lib/session-audit.d.ts +24 -0
  27. package/dist/lib/session-audit.js +94 -0
  28. package/dist/lib/session-parser.d.ts +35 -0
  29. package/dist/lib/session-parser.js +130 -0
  30. package/dist/lib/skill-mapper.d.ts +3 -0
  31. package/dist/lib/skill-mapper.js +173 -0
  32. package/dist/lib/skill-naming.d.ts +1 -0
  33. package/dist/lib/skill-naming.js +50 -0
  34. package/dist/lib/types.d.ts +17 -0
  35. package/dist/lib/types.js +2 -0
  36. package/dist/lib/workflow-detector.d.ts +11 -0
  37. package/dist/lib/workflow-detector.js +125 -0
  38. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -4,25 +4,44 @@ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, resolve } from "node:path";
6
6
  import { promisify } from "node:util";
7
+ import { buildAuditCliOptions, parseFlagBag, readOptionalString, } from "./lib/cli-args.js";
7
8
  const execFileAsync = promisify(execFile);
8
9
  const DEFAULT_RELAY_URL = "https://temet-relay.ramponneau.workers.dev/mcp";
9
10
  const DEFAULT_SERVER_NAME = "temet";
10
11
  const DEFAULT_PROTOCOL_APP_NAME = "Temet Handler";
11
12
  const LINUX_DESKTOP_ENTRY = "temet-handler.desktop";
12
13
  const LINUX_HANDLER_SCRIPT = "temet-protocol-handler";
13
- const HELP = `Temet CLI
14
+ const HELP = `Temet CLI — discover the skills you already demonstrate in AI work
14
15
 
15
- Usage:
16
- \ttemet connect --address <16hex> --token <token> [--config-path <path>] [--relay-url <url>] [--name temet] [--dry-run]
17
- \ttemet connect-url --url "temet://connect?address=<16hex>&token=<token>&relay=<url>&name=temet" [--config-path <path>] [--name temet] [--relay-url <url>] [--dry-run]
18
- \ttemet install-handler [--dry-run]
19
- \ttemet uninstall-handler [--dry-run]
16
+ Commands:
17
+ \ttemet audit [--path <session-dir>] Analyze local sessions, surface skills and workflows
18
+ \ttemet install-hook Auto-audit after every Claude Code session
19
+ \ttemet uninstall-hook Remove the SessionEnd hook
20
+ \ttemet connect --address <hex> --token <t> Connect your MCP client to your Temet card
21
+ \ttemet install-handler Register temet:// protocol handler
22
+
23
+ Audit options:
24
+ \t--path <dir> Directory containing .jsonl session files (auto-detected if omitted)
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
+ \t--json Output structured JSON instead of terminal display
28
+ \t--quiet Suppress all output (for background hooks)
29
+ \t--notify Send an OS notification on skill changes (used with --track)
30
+ \t--publish Publish results to your Temet card (requires confirmation)
31
+ \t--yes, -y Skip publish confirmation (for scripts/CI)
32
+
33
+ Advanced:
34
+ \t--narrate Enrich results with an LLM (requires model access: ANTHROPIC_API_KEY or CLAUDE_AUTH_TOKEN)
35
+ \t--model <id> Model to use for narration (default: claude-haiku-4-5-20251001)
20
36
 
21
37
  Examples:
22
- \ttemet connect --address a3f8c2d19f0b7e41 --token <token>
23
- \ttemet connect-url --url "temet://connect?address=a3f8c2d19f0b7e41&token=<token>&relay=https%3A%2F%2Ftemet-relay.ramponneau.workers.dev%2Fmcp"
24
- \tpnpm dlx @temet/cli install-handler
25
- \tpnpm dlx @temet/cli connect-url --url "temet://connect?address=a3f8c2d19f0b7e41&token=<token>"
38
+ \ttemet audit Auto-detect sessions from cwd
39
+ \ttemet audit Open the text report by default
40
+ \ttemet audit --path ~/.claude/projects/my-project
41
+ \ttemet audit --path ~/.claude/projects/my-project --track
42
+ \ttemet audit --path ~/.claude/projects/my-project --open-report
43
+ \ttemet audit --path ~/.claude/projects/my-project --json
44
+ \ttemet install-hook Background audit on session end
26
45
  `;
27
46
  function printHelp(exitCode = 0) {
28
47
  console.log(HELP);
@@ -38,36 +57,6 @@ function normalizeAddress(raw) {
38
57
  }
39
58
  return address;
40
59
  }
41
- function parseFlagBag(args) {
42
- const flags = new Map();
43
- const positionals = [];
44
- for (let i = 0; i < args.length; i++) {
45
- const arg = args[i];
46
- if (!arg.startsWith("--")) {
47
- positionals.push(arg);
48
- continue;
49
- }
50
- if (arg === "--dry-run") {
51
- flags.set("dry-run", true);
52
- continue;
53
- }
54
- const key = arg.slice(2);
55
- const value = args[i + 1];
56
- if (!value || value.startsWith("--")) {
57
- throw new Error(`Missing value for --${key}`);
58
- }
59
- flags.set(key, value);
60
- i += 1;
61
- }
62
- return { flags, positionals };
63
- }
64
- function readOptionalString(flags, key) {
65
- const value = flags.get(key);
66
- if (typeof value !== "string")
67
- return null;
68
- const trimmed = value.trim();
69
- return trimmed.length > 0 ? trimmed : null;
70
- }
71
60
  function parseConnectOptions(flags) {
72
61
  const address = normalizeAddress(String(flags.get("address") ?? ""));
73
62
  const token = String(flags.get("token") ?? "").trim();
@@ -156,6 +145,12 @@ function parseArgs(argv) {
156
145
  const [command, ...rest] = argv;
157
146
  const { flags, positionals } = parseFlagBag(rest);
158
147
  const dryRun = Boolean(flags.get("dry-run"));
148
+ if (command === "audit") {
149
+ return {
150
+ command,
151
+ options: buildAuditCliOptions(flags, process.env),
152
+ };
153
+ }
159
154
  if (command === "connect") {
160
155
  return {
161
156
  command,
@@ -171,6 +166,9 @@ function parseArgs(argv) {
171
166
  if (command === "install-handler" || command === "uninstall-handler") {
172
167
  return { command, dryRun };
173
168
  }
169
+ if (command === "install-hook" || command === "uninstall-hook") {
170
+ return { command, dryRun };
171
+ }
174
172
  console.error(`Unknown command: ${command}`);
175
173
  printHelp(1);
176
174
  }
@@ -491,10 +489,57 @@ async function uninstallProtocolHandler(dryRun) {
491
489
  }
492
490
  async function run() {
493
491
  const parsed = parseArgs(process.argv.slice(2));
492
+ if (parsed.command === "audit") {
493
+ const { runAuditCommand } = await import("./audit.js");
494
+ await runAuditCommand(parsed.options);
495
+ return;
496
+ }
494
497
  if (parsed.command === "connect" || parsed.command === "connect-url") {
495
498
  await runWriteFlow(parsed.command, parsed.options);
496
499
  return;
497
500
  }
501
+ if (parsed.command === "install-hook") {
502
+ const { resolveTemetBinary, readSettings, writeSettings, isHookInstalled, installHook, getSettingsPath, } = await import("./lib/hook-installer.js");
503
+ const settingsPath = getSettingsPath();
504
+ const settings = readSettings(settingsPath);
505
+ if (isHookInstalled(settings)) {
506
+ console.log("[temet] Hook already installed.");
507
+ return;
508
+ }
509
+ const binary = resolveTemetBinary();
510
+ if (!binary) {
511
+ console.error("[temet] Could not resolve temet binary. Install @temet/cli globally first: npm i -g @temet/cli");
512
+ process.exit(1);
513
+ }
514
+ if (parsed.dryRun) {
515
+ console.log(`[temet] dry-run: binary resolved to: ${binary}`);
516
+ console.log(`[temet] dry-run: would write to ${settingsPath}`);
517
+ return;
518
+ }
519
+ const updated = installHook(settings, binary);
520
+ writeSettings(settingsPath, updated);
521
+ console.log(`[temet] Resolved binary: ${binary}`);
522
+ console.log(`[temet] SessionEnd hook installed in ${settingsPath}`);
523
+ console.log("[temet] Restart Claude Code for the hook to take effect.");
524
+ return;
525
+ }
526
+ if (parsed.command === "uninstall-hook") {
527
+ const { readSettings, writeSettings, isHookInstalled, uninstallHook, getSettingsPath, } = await import("./lib/hook-installer.js");
528
+ const settingsPath = getSettingsPath();
529
+ const settings = readSettings(settingsPath);
530
+ if (!isHookInstalled(settings)) {
531
+ console.log("[temet] No hook installed.");
532
+ return;
533
+ }
534
+ if (parsed.dryRun) {
535
+ console.log(`[temet] dry-run: would remove hook from ${settingsPath}`);
536
+ return;
537
+ }
538
+ const updated = uninstallHook(settings);
539
+ writeSettings(settingsPath, updated);
540
+ console.log("[temet] SessionEnd hook removed.");
541
+ return;
542
+ }
498
543
  if (parsed.command === "install-handler") {
499
544
  await installProtocolHandler(parsed.dryRun);
500
545
  console.log("[temet] next: click a Quick connect button in Temet.");
@@ -0,0 +1,7 @@
1
+ import type { SessionMessage, ToolCall } from "./session-parser.js";
2
+ export type CombinedStats = {
3
+ messages: SessionMessage[];
4
+ toolCalls: ToolCall[];
5
+ filesTouched: string[];
6
+ allToolCallsBySession: ToolCall[][];
7
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,55 @@
1
+ import type { AuditResult } from "./session-audit.js";
2
+ import type { CompetencyEntry, ProficiencyLevel } from "./types.js";
3
+ export type AuditSkillSnapshot = {
4
+ name: string;
5
+ category: string;
6
+ proficiencyLevel: ProficiencyLevel;
7
+ evidenceCount: number;
8
+ };
9
+ export type AuditWorkflowSnapshot = {
10
+ description: string;
11
+ sequence: string[];
12
+ occurrences: number;
13
+ sessions: number;
14
+ confidence: number;
15
+ };
16
+ export type AuditSnapshot = {
17
+ version: 1;
18
+ createdAt: string;
19
+ projectKey: string;
20
+ projectLabel: string;
21
+ sourcePath: string;
22
+ sessions: number;
23
+ messages: number;
24
+ toolCalls: number;
25
+ skills: AuditSkillSnapshot[];
26
+ workflows: AuditWorkflowSnapshot[];
27
+ };
28
+ export type AuditChange = {
29
+ type: "baseline";
30
+ title: string;
31
+ detail: string;
32
+ } | {
33
+ type: "new_skill" | "level_up" | "level_down" | "new_workflow";
34
+ title: string;
35
+ detail: string;
36
+ };
37
+ export type TrackingResult = {
38
+ previous: AuditSnapshot | null;
39
+ current: AuditSnapshot;
40
+ changes: AuditChange[];
41
+ latestPath: string;
42
+ historyPath: string;
43
+ skipped: boolean;
44
+ };
45
+ export declare function buildProjectKey(sourcePath: string): string;
46
+ export declare function buildAuditSnapshot(sourcePath: string, result: Pick<AuditResult, "sessionCount" | "messageCount" | "toolCallCount" | "workflows">, competencies: CompetencyEntry[]): AuditSnapshot;
47
+ export declare function loadLatestSnapshot(sourcePath: string): Promise<AuditSnapshot | null>;
48
+ export declare function saveAuditSnapshot(snapshot: AuditSnapshot): Promise<{
49
+ latestPath: string;
50
+ historyPath: string;
51
+ }>;
52
+ export declare function diffAuditSnapshots(previous: AuditSnapshot | null, current: AuditSnapshot): AuditChange[];
53
+ /** Check if two snapshots have identical skills and workflows (ignoring timestamps/counts). */
54
+ export declare function snapshotsEqual(a: AuditSnapshot, b: AuditSnapshot): boolean;
55
+ export declare function trackAuditSnapshot(sourcePath: string, result: Pick<AuditResult, "sessionCount" | "messageCount" | "toolCallCount" | "workflows">, competencies: CompetencyEntry[]): Promise<TrackingResult>;
@@ -0,0 +1,185 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ const PROFICIENCY_ORDER = {
6
+ novice: 0,
7
+ advanced_beginner: 1,
8
+ competent: 2,
9
+ proficient: 3,
10
+ expert: 4,
11
+ };
12
+ function normalizeName(value) {
13
+ return value.toLowerCase().replace(/\s+/g, " ").trim();
14
+ }
15
+ function evidenceCount(entry) {
16
+ return (entry.evidence.examples.length +
17
+ entry.evidence.decisionCriteria.length +
18
+ entry.evidence.antiPatterns.length +
19
+ entry.evidence.mentorAdvice.length);
20
+ }
21
+ export function buildProjectKey(sourcePath) {
22
+ return createHash("sha256")
23
+ .update(path.resolve(sourcePath))
24
+ .digest("hex")
25
+ .slice(0, 12);
26
+ }
27
+ function buildProjectLabel(sourcePath) {
28
+ const resolved = path.resolve(sourcePath);
29
+ const base = path.basename(resolved);
30
+ return base || resolved;
31
+ }
32
+ export function buildAuditSnapshot(sourcePath, result, competencies) {
33
+ return {
34
+ version: 1,
35
+ createdAt: new Date().toISOString(),
36
+ projectKey: buildProjectKey(sourcePath),
37
+ projectLabel: buildProjectLabel(sourcePath),
38
+ sourcePath: path.resolve(sourcePath),
39
+ sessions: result.sessionCount,
40
+ messages: result.messageCount,
41
+ toolCalls: result.toolCallCount,
42
+ skills: competencies
43
+ .map((entry) => ({
44
+ name: entry.name,
45
+ category: entry.category,
46
+ proficiencyLevel: entry.proficiencyLevel,
47
+ evidenceCount: evidenceCount(entry),
48
+ }))
49
+ .sort((a, b) => a.name.localeCompare(b.name)),
50
+ workflows: result.workflows
51
+ .map((workflow) => ({
52
+ description: workflow.description,
53
+ sequence: workflow.sequence,
54
+ occurrences: workflow.occurrences,
55
+ sessions: workflow.sessions,
56
+ confidence: workflow.confidence,
57
+ }))
58
+ .sort((a, b) => a.description.localeCompare(b.description)),
59
+ };
60
+ }
61
+ function getTrackingPaths(projectKey) {
62
+ const root = path.join(homedir(), ".temet", "audits", projectKey);
63
+ return {
64
+ root,
65
+ latest: path.join(root, "latest.json"),
66
+ historyDir: path.join(root, "history"),
67
+ };
68
+ }
69
+ export async function loadLatestSnapshot(sourcePath) {
70
+ const { latest } = getTrackingPaths(buildProjectKey(sourcePath));
71
+ try {
72
+ const raw = await readFile(latest, "utf8");
73
+ const parsed = JSON.parse(raw);
74
+ if (parsed?.version !== 1)
75
+ return null;
76
+ return parsed;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ export async function saveAuditSnapshot(snapshot) {
83
+ const paths = getTrackingPaths(snapshot.projectKey);
84
+ await mkdir(paths.historyDir, { recursive: true });
85
+ const historyPath = path.join(paths.historyDir, `${snapshot.createdAt.replaceAll(":", "-")}.json`);
86
+ const payload = JSON.stringify(snapshot, null, 2);
87
+ await writeFile(paths.latest, payload, "utf8");
88
+ await writeFile(historyPath, payload, "utf8");
89
+ return { latestPath: paths.latest, historyPath };
90
+ }
91
+ export function diffAuditSnapshots(previous, current) {
92
+ if (!previous) {
93
+ return [
94
+ {
95
+ type: "baseline",
96
+ title: "Tracking baseline created",
97
+ detail: `Saved the first audit snapshot for ${current.projectLabel}.`,
98
+ },
99
+ ];
100
+ }
101
+ const changes = [];
102
+ const previousSkills = new Map(previous.skills.map((skill) => [normalizeName(skill.name), skill]));
103
+ const currentSkills = new Map(current.skills.map((skill) => [normalizeName(skill.name), skill]));
104
+ for (const [key, skill] of currentSkills) {
105
+ const before = previousSkills.get(key);
106
+ if (!before) {
107
+ if (PROFICIENCY_ORDER[skill.proficiencyLevel] >=
108
+ PROFICIENCY_ORDER.competent ||
109
+ skill.evidenceCount >= 2) {
110
+ changes.push({
111
+ type: "new_skill",
112
+ title: `New skill surfaced: ${skill.name}`,
113
+ detail: `${skill.proficiencyLevel.replace("_", " ")} with ${skill.evidenceCount} evidence item(s).`,
114
+ });
115
+ }
116
+ continue;
117
+ }
118
+ const beforeLevel = PROFICIENCY_ORDER[before.proficiencyLevel];
119
+ const currentLevel = PROFICIENCY_ORDER[skill.proficiencyLevel];
120
+ if (currentLevel > beforeLevel) {
121
+ changes.push({
122
+ type: "level_up",
123
+ title: `${skill.name} moved up`,
124
+ detail: `${before.proficiencyLevel.replace("_", " ")} → ${skill.proficiencyLevel.replace("_", " ")}`,
125
+ });
126
+ }
127
+ else if (currentLevel < beforeLevel) {
128
+ changes.push({
129
+ type: "level_down",
130
+ title: `${skill.name} moved down`,
131
+ detail: `${before.proficiencyLevel.replace("_", " ")} → ${skill.proficiencyLevel.replace("_", " ")}`,
132
+ });
133
+ }
134
+ }
135
+ const previousWorkflows = new Set(previous.workflows.map((workflow) => normalizeName(workflow.description)));
136
+ for (const workflow of current.workflows) {
137
+ const key = normalizeName(workflow.description);
138
+ if (!previousWorkflows.has(key)) {
139
+ changes.push({
140
+ type: "new_workflow",
141
+ title: `New repeated pattern: ${workflow.description}`,
142
+ detail: `${workflow.occurrences} occurrences across ${workflow.sessions} sessions.`,
143
+ });
144
+ }
145
+ }
146
+ const order = {
147
+ baseline: 0,
148
+ level_up: 1,
149
+ new_skill: 2,
150
+ new_workflow: 3,
151
+ level_down: 4,
152
+ };
153
+ return changes.sort((a, b) => order[a.type] - order[b.type]).slice(0, 8);
154
+ }
155
+ /** Check if two snapshots have identical skills and workflows (ignoring timestamps/counts). */
156
+ export function snapshotsEqual(a, b) {
157
+ return (JSON.stringify(a.skills) === JSON.stringify(b.skills) &&
158
+ JSON.stringify(a.workflows) === JSON.stringify(b.workflows));
159
+ }
160
+ export async function trackAuditSnapshot(sourcePath, result, competencies) {
161
+ const previous = await loadLatestSnapshot(sourcePath);
162
+ const current = buildAuditSnapshot(sourcePath, result, competencies);
163
+ // No-op guard: skip save if skills and workflows are identical
164
+ if (previous && snapshotsEqual(previous, current)) {
165
+ const paths = getTrackingPaths(current.projectKey);
166
+ return {
167
+ previous,
168
+ current,
169
+ changes: [],
170
+ latestPath: paths.latest,
171
+ historyPath: "",
172
+ skipped: true,
173
+ };
174
+ }
175
+ const changes = diffAuditSnapshots(previous, current);
176
+ const { latestPath, historyPath } = await saveAuditSnapshot(current);
177
+ return {
178
+ previous,
179
+ current,
180
+ changes,
181
+ latestPath,
182
+ historyPath,
183
+ skipped: false,
184
+ };
185
+ }
@@ -0,0 +1,22 @@
1
+ export type FlagValue = string | boolean;
2
+ export type AuditCliOptions = {
3
+ path: string;
4
+ track: boolean;
5
+ narrate: boolean;
6
+ openReport: boolean;
7
+ json: boolean;
8
+ quiet: boolean;
9
+ notify: boolean;
10
+ publish: boolean;
11
+ yes: boolean;
12
+ model: string;
13
+ address: string;
14
+ token: string;
15
+ relayUrl: string;
16
+ };
17
+ export declare function parseFlagBag(args: string[]): {
18
+ flags: Map<string, FlagValue>;
19
+ positionals: string[];
20
+ };
21
+ export declare function readOptionalString(flags: Map<string, FlagValue>, key: string): string | null;
22
+ export declare function buildAuditCliOptions(flags: Map<string, FlagValue>, env: NodeJS.ProcessEnv): AuditCliOptions;
@@ -0,0 +1,65 @@
1
+ export function parseFlagBag(args) {
2
+ const flags = new Map();
3
+ const positionals = [];
4
+ for (let i = 0; i < args.length; i++) {
5
+ const arg = args[i];
6
+ if (arg === "-y") {
7
+ flags.set("yes", true);
8
+ continue;
9
+ }
10
+ if (!arg.startsWith("--")) {
11
+ positionals.push(arg);
12
+ continue;
13
+ }
14
+ if (arg === "--dry-run" ||
15
+ arg === "--track" ||
16
+ arg === "--narrate" ||
17
+ arg === "--open-report" ||
18
+ arg === "--json" ||
19
+ arg === "--quiet" ||
20
+ arg === "--notify" ||
21
+ arg === "--publish" ||
22
+ arg === "--yes") {
23
+ flags.set(arg.slice(2), true);
24
+ continue;
25
+ }
26
+ const key = arg.slice(2);
27
+ const value = args[i + 1];
28
+ if (!value || value.startsWith("--")) {
29
+ throw new Error(`Missing value for --${key}`);
30
+ }
31
+ flags.set(key, value);
32
+ i += 1;
33
+ }
34
+ return { flags, positionals };
35
+ }
36
+ export function readOptionalString(flags, key) {
37
+ const value = flags.get(key);
38
+ if (typeof value !== "string")
39
+ return null;
40
+ const trimmed = value.trim();
41
+ return trimmed.length > 0 ? trimmed : null;
42
+ }
43
+ export function buildAuditCliOptions(flags, env) {
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"));
48
+ return {
49
+ path: pathVal,
50
+ track: Boolean(flags.get("track")),
51
+ narrate: Boolean(flags.get("narrate")),
52
+ openReport: explicitOpenReport || (!json && !quiet),
53
+ json,
54
+ quiet,
55
+ notify: Boolean(flags.get("notify")),
56
+ publish: Boolean(flags.get("publish")),
57
+ yes: Boolean(flags.get("yes")),
58
+ model: readOptionalString(flags, "model") ?? "",
59
+ address: readOptionalString(flags, "address") ?? env.TEMET_ADDRESS ?? "",
60
+ token: readOptionalString(flags, "token") ?? env.TEMET_TOKEN ?? "",
61
+ relayUrl: readOptionalString(flags, "relay-url") ??
62
+ env.RELAY_URL ??
63
+ "https://temet-relay.ramponneau.workers.dev",
64
+ };
65
+ }
@@ -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
+ }
@@ -0,0 +1,15 @@
1
+ import type { SessionStats } from "./session-parser.js";
2
+ export type Signal = {
3
+ type: string;
4
+ skill: string;
5
+ confidence: number;
6
+ evidence: string;
7
+ category?: string;
8
+ };
9
+ export declare function extractCorrectionPatterns(stats: SessionStats): Signal[];
10
+ export declare function extractToolFrequency(stats: SessionStats): Signal[];
11
+ export declare function extractDecisionLanguage(stats: SessionStats): Signal[];
12
+ export declare function extractDomainClusters(stats: SessionStats): Signal[];
13
+ export declare function extractPromptStructure(stats: SessionStats): Signal[];
14
+ export declare const ALL_EXTRACTORS: readonly [typeof extractCorrectionPatterns, typeof extractToolFrequency, typeof extractDecisionLanguage, typeof extractDomainClusters, typeof extractPromptStructure];
15
+ export declare function extractAllSignals(stats: SessionStats): Signal[];