@temet/cli 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/macos/README.md +8 -0
- package/assets/macos/Temet.app.zip +0 -0
- package/dist/audit.d.ts +3 -1
- package/dist/audit.js +214 -26
- package/dist/index.js +60 -12
- package/dist/lib/audit-tracking.js +13 -2
- package/dist/lib/cli-args.d.ts +1 -0
- package/dist/lib/cli-args.js +13 -3
- package/dist/lib/diagnostic-runner.d.ts +13 -0
- package/dist/lib/diagnostic-runner.js +95 -0
- package/dist/lib/editorial-taxonomy.d.ts +17 -0
- package/dist/lib/editorial-taxonomy.js +91 -0
- package/dist/lib/menubar-installer.d.ts +19 -0
- package/dist/lib/menubar-installer.js +99 -0
- package/dist/lib/menubar-state.d.ts +78 -0
- package/dist/lib/menubar-state.js +275 -0
- package/dist/lib/notifier.d.ts +2 -0
- package/dist/lib/notifier.js +11 -0
- package/dist/lib/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 +9 -0
- package/dist/lib/report-writer.js +91 -0
- package/dist/lib/session-audit.d.ts +1 -0
- package/dist/lib/session-audit.js +2 -0
- package/dist/plan.d.ts +33 -0
- package/dist/plan.js +90 -0
- package/dist/traces.d.ts +41 -0
- package/dist/traces.js +119 -0
- package/package.json +3 -2
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { resolveSessionPath } from "./path-resolver.js";
|
|
4
|
+
import { trackAuditSnapshot } from "./audit-tracking.js";
|
|
5
|
+
import { findSessionFiles, runAudit, } from "./session-audit.js";
|
|
6
|
+
async function chooseSessionCandidate(candidates) {
|
|
7
|
+
if (!process.stdin.isTTY || candidates.length === 0)
|
|
8
|
+
return null;
|
|
9
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
10
|
+
process.stderr.write(`\n[temet] I found ${candidates.length} Claude Code projects with session history. Pick one to inspect.\n\n`);
|
|
11
|
+
candidates.slice(0, 9).forEach((candidate, index) => {
|
|
12
|
+
process.stderr.write(` ${index + 1}. ${candidate.label}${index === 0 ? " (most recent)" : ""}\n`);
|
|
13
|
+
});
|
|
14
|
+
process.stderr.write("\n");
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
rl.question("[temet] Choose 1-9, or press Enter for the most recent project: ", (answer) => {
|
|
17
|
+
rl.close();
|
|
18
|
+
const trimmed = answer.trim();
|
|
19
|
+
if (!trimmed) {
|
|
20
|
+
resolve(candidates[0] ?? null);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const index = Number.parseInt(trimmed, 10) - 1;
|
|
24
|
+
if (!Number.isFinite(index) ||
|
|
25
|
+
index < 0 ||
|
|
26
|
+
index >= candidates.length) {
|
|
27
|
+
resolve(candidates[0] ?? null);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
resolve(candidates[index] ?? null);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export async function resolveDiagnostics(opts) {
|
|
35
|
+
const resolution = resolveSessionPath(opts.path || undefined, process.env);
|
|
36
|
+
if (!resolution.path) {
|
|
37
|
+
throw new Error("No Claude Code sessions found here. Run this command from a project folder, or use --path <session-dir>.");
|
|
38
|
+
}
|
|
39
|
+
let resolvedPath = resolution.path;
|
|
40
|
+
if (!opts.path &&
|
|
41
|
+
!opts.json &&
|
|
42
|
+
!opts.quiet &&
|
|
43
|
+
resolution.source === "recent" &&
|
|
44
|
+
resolution.candidates.length > 1) {
|
|
45
|
+
const selected = await chooseSessionCandidate(resolution.candidates);
|
|
46
|
+
resolvedPath = selected?.sessionDir ?? resolution.path;
|
|
47
|
+
const label = selected?.label ?? resolution.candidates[0]?.label;
|
|
48
|
+
if (label) {
|
|
49
|
+
process.stderr.write(`[temet] using ${label}\n`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const label = resolution.candidates[0]?.label;
|
|
54
|
+
if (label && resolution.source !== "explicit" && !opts.quiet) {
|
|
55
|
+
process.stderr.write(`[temet] using ${label}\n`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const sessionFiles = findSessionFiles(resolvedPath);
|
|
59
|
+
if (sessionFiles.length === 0) {
|
|
60
|
+
throw new Error(`No .jsonl session files found in ${resolvedPath}.`);
|
|
61
|
+
}
|
|
62
|
+
if (!opts.quiet) {
|
|
63
|
+
process.stderr.write(`[temet] reading ${sessionFiles.length} saved session file(s)...\n`);
|
|
64
|
+
}
|
|
65
|
+
const result = await runAudit(sessionFiles);
|
|
66
|
+
let competencies = result.competencies;
|
|
67
|
+
let bilan;
|
|
68
|
+
if (opts.narrate && !opts.quiet) {
|
|
69
|
+
try {
|
|
70
|
+
const narratorModule = await import("./narrator-lite.js");
|
|
71
|
+
if (narratorModule.resolveAuth()) {
|
|
72
|
+
const narrated = await narratorModule.narrateCompetencies(competencies, result.combined, result.workflows, { model: opts.model || undefined });
|
|
73
|
+
competencies = narrated.competencies;
|
|
74
|
+
bilan = narrated.bilan || undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Keep deterministic fallback if narration fails.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let tracking;
|
|
82
|
+
if (opts.track) {
|
|
83
|
+
tracking = await trackAuditSnapshot(resolvedPath, result, competencies);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
resolvedPath,
|
|
87
|
+
result,
|
|
88
|
+
competencies,
|
|
89
|
+
bilan,
|
|
90
|
+
tracking,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function defaultProjectLabel(resolvedPath) {
|
|
94
|
+
return basename(resolvedPath);
|
|
95
|
+
}
|
|
@@ -0,0 +1,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,19 @@
|
|
|
1
|
+
export declare const TEMET_BAR_APP_NAME = "Temet.app";
|
|
2
|
+
export type MenubarInstallResult = {
|
|
3
|
+
appPath: string;
|
|
4
|
+
configPath: string;
|
|
5
|
+
hookInstalled: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function getMenubarAppPath(): string;
|
|
8
|
+
export declare function isMenubarInstalled(): boolean;
|
|
9
|
+
export declare function getMenubarAssetPath(): string;
|
|
10
|
+
export declare function installMenubar(options?: {
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
ensureHook?: boolean;
|
|
13
|
+
}): Promise<MenubarInstallResult>;
|
|
14
|
+
export declare function uninstallMenubar(options?: {
|
|
15
|
+
dryRun?: boolean;
|
|
16
|
+
}): Promise<{
|
|
17
|
+
appPath: string;
|
|
18
|
+
configPath: string;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { getSettingsPath, installHook, isHookInstalled, readSettings, resolveTemetBinary, writeSettings, } from "./hook-installer.js";
|
|
9
|
+
import { getMenubarConfigPath, writeMenubarConfig } from "./menubar-state.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
export const TEMET_BAR_APP_NAME = "Temet.app";
|
|
12
|
+
function findCliPackageRoot() {
|
|
13
|
+
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
while (true) {
|
|
15
|
+
const packageJson = path.join(current, "package.json");
|
|
16
|
+
if (existsSync(packageJson)) {
|
|
17
|
+
return current;
|
|
18
|
+
}
|
|
19
|
+
const parent = path.dirname(current);
|
|
20
|
+
if (parent === current) {
|
|
21
|
+
throw new Error("Could not resolve @temet/cli package root.");
|
|
22
|
+
}
|
|
23
|
+
current = parent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function getMenubarAppPath() {
|
|
27
|
+
return path.join(homedir(), "Applications", TEMET_BAR_APP_NAME);
|
|
28
|
+
}
|
|
29
|
+
export function isMenubarInstalled() {
|
|
30
|
+
return existsSync(getMenubarAppPath());
|
|
31
|
+
}
|
|
32
|
+
export function getMenubarAssetPath() {
|
|
33
|
+
return path.join(findCliPackageRoot(), "assets", "macos", "Temet.app.zip");
|
|
34
|
+
}
|
|
35
|
+
async function bestEffortClearQuarantine(appPath) {
|
|
36
|
+
try {
|
|
37
|
+
await execFileAsync("xattr", ["-d", "com.apple.quarantine", appPath]);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Best effort only.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function installMenubar(options) {
|
|
44
|
+
if (process.platform !== "darwin") {
|
|
45
|
+
throw new Error("Temet is only available in the macOS menu bar.");
|
|
46
|
+
}
|
|
47
|
+
const assetPath = getMenubarAssetPath();
|
|
48
|
+
if (!existsSync(assetPath)) {
|
|
49
|
+
throw new Error(`Missing Temet.app.zip in ${assetPath}. Build and package the menu bar app first.`);
|
|
50
|
+
}
|
|
51
|
+
const binary = resolveTemetBinary();
|
|
52
|
+
if (!binary) {
|
|
53
|
+
throw new Error("Could not resolve the temet binary. Install @temet/cli globally first.");
|
|
54
|
+
}
|
|
55
|
+
const appPath = getMenubarAppPath();
|
|
56
|
+
const configPath = getMenubarConfigPath();
|
|
57
|
+
let hookInstalled = false;
|
|
58
|
+
if (options?.dryRun) {
|
|
59
|
+
return { appPath, configPath, hookInstalled: options.ensureHook === true };
|
|
60
|
+
}
|
|
61
|
+
await mkdir(path.dirname(appPath), { recursive: true });
|
|
62
|
+
await rm(appPath, { recursive: true, force: true });
|
|
63
|
+
await execFileAsync("unzip", ["-oq", assetPath, "-d", path.dirname(appPath)]);
|
|
64
|
+
await writeMenubarConfig(binary);
|
|
65
|
+
if (options?.ensureHook) {
|
|
66
|
+
const settingsPath = getSettingsPath();
|
|
67
|
+
const settings = readSettings(settingsPath);
|
|
68
|
+
if (!isHookInstalled(settings)) {
|
|
69
|
+
const updated = installHook(settings, binary);
|
|
70
|
+
writeSettings(settingsPath, updated);
|
|
71
|
+
hookInstalled = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
await bestEffortClearQuarantine(appPath);
|
|
75
|
+
try {
|
|
76
|
+
await execFileAsync("open", [appPath]);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
throw new Error(`Temet was installed but macOS refused to open it automatically. Open ${appPath} manually. Details: ${message}`);
|
|
81
|
+
}
|
|
82
|
+
return { appPath, configPath, hookInstalled };
|
|
83
|
+
}
|
|
84
|
+
export async function uninstallMenubar(options) {
|
|
85
|
+
const appPath = getMenubarAppPath();
|
|
86
|
+
const configPath = getMenubarConfigPath();
|
|
87
|
+
if (options?.dryRun) {
|
|
88
|
+
return { appPath, configPath };
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
await execFileAsync("pkill", ["-f", "Temet.app"]);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// App may not be running.
|
|
95
|
+
}
|
|
96
|
+
await rm(appPath, { recursive: true, force: true });
|
|
97
|
+
await rm(configPath, { force: true });
|
|
98
|
+
return { appPath, configPath };
|
|
99
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type TrackingResult } from "./audit-tracking.js";
|
|
2
|
+
import type { AuditResult } from "./session-audit.js";
|
|
3
|
+
import type { CompetencyEntry, ProficiencyLevel } from "./types.js";
|
|
4
|
+
export type MenubarSuperpower = {
|
|
5
|
+
title: string;
|
|
6
|
+
level: ProficiencyLevel;
|
|
7
|
+
evidenceCount: number;
|
|
8
|
+
};
|
|
9
|
+
export type MenubarWatch = {
|
|
10
|
+
title: string;
|
|
11
|
+
body: string;
|
|
12
|
+
};
|
|
13
|
+
export type CurrentFocusStatus = "active" | "improving" | "missed" | "completed" | "replaced";
|
|
14
|
+
export type CurrentFocusCheck = "followed" | "not_followed" | "insufficient_signal";
|
|
15
|
+
export type MenubarCurrentFocus = {
|
|
16
|
+
title: string;
|
|
17
|
+
reason: string;
|
|
18
|
+
startedAt: string;
|
|
19
|
+
durationDays: number;
|
|
20
|
+
status: CurrentFocusStatus;
|
|
21
|
+
lastCheck: CurrentFocusCheck;
|
|
22
|
+
streak: number;
|
|
23
|
+
};
|
|
24
|
+
export type MenubarMetrics = {
|
|
25
|
+
sessions: number;
|
|
26
|
+
prompts: number;
|
|
27
|
+
toolCalls: number;
|
|
28
|
+
workflows: number;
|
|
29
|
+
};
|
|
30
|
+
export type MenubarProgress = {
|
|
31
|
+
summary: string;
|
|
32
|
+
reinforced: string;
|
|
33
|
+
watch: string;
|
|
34
|
+
new: string;
|
|
35
|
+
next: string;
|
|
36
|
+
notificationTitle: string;
|
|
37
|
+
notificationBody: string;
|
|
38
|
+
};
|
|
39
|
+
export type TemetMenubarState = {
|
|
40
|
+
version: 1;
|
|
41
|
+
projectKey: string;
|
|
42
|
+
projectLabel: string;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
trackingInstalled: boolean;
|
|
45
|
+
metrics: MenubarMetrics;
|
|
46
|
+
dna: string;
|
|
47
|
+
superpower: MenubarSuperpower;
|
|
48
|
+
watch: MenubarWatch;
|
|
49
|
+
progress: MenubarProgress;
|
|
50
|
+
recentChange: string;
|
|
51
|
+
currentFocus: MenubarCurrentFocus;
|
|
52
|
+
lastReportPath: string | null;
|
|
53
|
+
sourcePath: string;
|
|
54
|
+
};
|
|
55
|
+
export type MenubarConfig = {
|
|
56
|
+
version: 1;
|
|
57
|
+
temetBinary: string;
|
|
58
|
+
};
|
|
59
|
+
type BuildMenubarStateInput = {
|
|
60
|
+
sourcePath: string;
|
|
61
|
+
result: Pick<AuditResult, "sessionCount" | "promptCount" | "messageCount" | "toolCallCount" | "workflowCount" | "workflows" | "signals">;
|
|
62
|
+
competencies: CompetencyEntry[];
|
|
63
|
+
bilan?: string;
|
|
64
|
+
tracking?: TrackingResult;
|
|
65
|
+
trackingInstalled: boolean;
|
|
66
|
+
lastReportPath?: string | null;
|
|
67
|
+
previousState?: TemetMenubarState | null;
|
|
68
|
+
};
|
|
69
|
+
export declare function getMenubarStatePath(): string;
|
|
70
|
+
export declare function getMenubarConfigPath(): string;
|
|
71
|
+
export declare function buildMenubarState(input: BuildMenubarStateInput): TemetMenubarState;
|
|
72
|
+
export declare function loadMenubarState(): Promise<TemetMenubarState | null>;
|
|
73
|
+
export declare function writeMenubarState(input: BuildMenubarStateInput): Promise<TemetMenubarState>;
|
|
74
|
+
export declare function loadMenubarConfig(): Promise<MenubarConfig | null>;
|
|
75
|
+
export declare function writeMenubarConfig(temetBinary: string): Promise<MenubarConfig>;
|
|
76
|
+
export declare function isMenubarConfigPresent(): boolean;
|
|
77
|
+
export declare function resolveLastReportPath(projectLabel: string): Promise<string | null>;
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { buildAuditSnapshot, } from "./audit-tracking.js";
|
|
6
|
+
import { buildSkillAuditReport, } from "./profile-report.js";
|
|
7
|
+
import { findLatestReportPath } from "./report-writer.js";
|
|
8
|
+
const PROFICIENCY_SCORE = {
|
|
9
|
+
novice: 1,
|
|
10
|
+
advanced_beginner: 2,
|
|
11
|
+
competent: 3,
|
|
12
|
+
proficient: 4,
|
|
13
|
+
expert: 5,
|
|
14
|
+
};
|
|
15
|
+
function getTemetRoot() {
|
|
16
|
+
return path.join(homedir(), ".temet");
|
|
17
|
+
}
|
|
18
|
+
function getMenubarRoot() {
|
|
19
|
+
return path.join(getTemetRoot(), "menubar");
|
|
20
|
+
}
|
|
21
|
+
export function getMenubarStatePath() {
|
|
22
|
+
return path.join(getMenubarRoot(), "latest.json");
|
|
23
|
+
}
|
|
24
|
+
export function getMenubarConfigPath() {
|
|
25
|
+
return path.join(getMenubarRoot(), "config.json");
|
|
26
|
+
}
|
|
27
|
+
function evidenceCount(entry) {
|
|
28
|
+
return (entry.evidence.examples.length +
|
|
29
|
+
entry.evidence.decisionCriteria.length +
|
|
30
|
+
entry.evidence.antiPatterns.length +
|
|
31
|
+
entry.evidence.mentorAdvice.length);
|
|
32
|
+
}
|
|
33
|
+
function pickTopCompetency(competencies) {
|
|
34
|
+
return [...competencies].sort((a, b) => {
|
|
35
|
+
const scoreA = PROFICIENCY_SCORE[a.proficiencyLevel] * 10 + evidenceCount(a);
|
|
36
|
+
const scoreB = PROFICIENCY_SCORE[b.proficiencyLevel] * 10 + evidenceCount(b);
|
|
37
|
+
if (scoreA !== scoreB)
|
|
38
|
+
return scoreB - scoreA;
|
|
39
|
+
return a.name.localeCompare(b.name);
|
|
40
|
+
})[0];
|
|
41
|
+
}
|
|
42
|
+
function firstSentence(value) {
|
|
43
|
+
const trimmed = value.replace(/\s+/g, " ").trim();
|
|
44
|
+
if (!trimmed)
|
|
45
|
+
return "";
|
|
46
|
+
const match = trimmed.match(/^(.+?[.!?])(?:\s|$)/);
|
|
47
|
+
return (match?.[1] ?? trimmed).trim();
|
|
48
|
+
}
|
|
49
|
+
function sanitizeLine(value, fallback) {
|
|
50
|
+
const trimmed = value.replace(/\s+/g, " ").trim();
|
|
51
|
+
return trimmed.length > 0 ? trimmed : fallback;
|
|
52
|
+
}
|
|
53
|
+
function trimPreview(value, maxLength) {
|
|
54
|
+
const normalized = sanitizeLine(value, "");
|
|
55
|
+
if (normalized.length <= maxLength)
|
|
56
|
+
return normalized;
|
|
57
|
+
return `${normalized.slice(0, maxLength).trimEnd()}...`;
|
|
58
|
+
}
|
|
59
|
+
function humanizeChange(change) {
|
|
60
|
+
switch (change.type) {
|
|
61
|
+
case "new_skill":
|
|
62
|
+
return change.title.replace(/^New skill surfaced:\s*/i, "New skill: ");
|
|
63
|
+
case "new_workflow":
|
|
64
|
+
return "A new workflow signal surfaced";
|
|
65
|
+
case "level_up":
|
|
66
|
+
return change.title.replace(/\s*moved up$/i, " is strengthening");
|
|
67
|
+
case "level_down":
|
|
68
|
+
return change.title.replace(/\s*moved down$/i, " needs attention");
|
|
69
|
+
case "baseline":
|
|
70
|
+
return "Tracking baseline created";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function selectCurrentFocusTemplate(watch) {
|
|
74
|
+
switch (watch.title) {
|
|
75
|
+
case "Feature sprawl":
|
|
76
|
+
return {
|
|
77
|
+
title: "Validate demand before adding product surface",
|
|
78
|
+
reason: firstSentence(watch.body),
|
|
79
|
+
durationDays: 7,
|
|
80
|
+
};
|
|
81
|
+
case "Git hygiene under pressure":
|
|
82
|
+
return {
|
|
83
|
+
title: "Tighten git hygiene before pushing",
|
|
84
|
+
reason: firstSentence(watch.body),
|
|
85
|
+
durationDays: 7,
|
|
86
|
+
};
|
|
87
|
+
case "Technical detours":
|
|
88
|
+
return {
|
|
89
|
+
title: "Stop exploration once product value flattens",
|
|
90
|
+
reason: firstSentence(watch.body),
|
|
91
|
+
durationDays: 7,
|
|
92
|
+
};
|
|
93
|
+
case "Coordination drag":
|
|
94
|
+
return {
|
|
95
|
+
title: "Name the next decision before expanding scope",
|
|
96
|
+
reason: firstSentence(watch.body),
|
|
97
|
+
durationDays: 7,
|
|
98
|
+
};
|
|
99
|
+
default:
|
|
100
|
+
return {
|
|
101
|
+
title: `Address ${watch.title.toLowerCase()}`,
|
|
102
|
+
reason: firstSentence(watch.body),
|
|
103
|
+
durationDays: 7,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function evaluateCurrentFocus(watch, createdAt, previousState) {
|
|
108
|
+
const template = selectCurrentFocusTemplate(watch);
|
|
109
|
+
if (!previousState) {
|
|
110
|
+
return {
|
|
111
|
+
...template,
|
|
112
|
+
startedAt: createdAt,
|
|
113
|
+
status: "active",
|
|
114
|
+
lastCheck: "insufficient_signal",
|
|
115
|
+
streak: 0,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (previousState.currentFocus.title !== template.title) {
|
|
119
|
+
return {
|
|
120
|
+
...template,
|
|
121
|
+
startedAt: createdAt,
|
|
122
|
+
status: "active",
|
|
123
|
+
lastCheck: "insufficient_signal",
|
|
124
|
+
streak: 0,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
...template,
|
|
129
|
+
startedAt: previousState.currentFocus.startedAt,
|
|
130
|
+
status: "active",
|
|
131
|
+
lastCheck: "insufficient_signal",
|
|
132
|
+
streak: 0,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function buildProgress(currentFocus, watch, changes, previousState) {
|
|
136
|
+
const meaningfulChanges = changes.filter((change) => change.type !== "baseline");
|
|
137
|
+
const firstMeaningful = meaningfulChanges[0];
|
|
138
|
+
const isNewFocus = previousState?.currentFocus.title != null &&
|
|
139
|
+
previousState.currentFocus.title !== currentFocus.title;
|
|
140
|
+
const humanizedSignal = firstMeaningful
|
|
141
|
+
? humanizeChange(firstMeaningful)
|
|
142
|
+
: null;
|
|
143
|
+
const reinforced = currentFocus.lastCheck === "followed"
|
|
144
|
+
? currentFocus.title
|
|
145
|
+
: (humanizedSignal ?? currentFocus.title);
|
|
146
|
+
const watchLine = currentFocus.lastCheck === "not_followed"
|
|
147
|
+
? "Last session drifted away from this focus."
|
|
148
|
+
: firstSentence(watch.body);
|
|
149
|
+
const newSignal = humanizedSignal ?? "No new signal surfaced yet";
|
|
150
|
+
const next = "Open plan to review the operating adjustment.";
|
|
151
|
+
let summary = "Set today. Temet will start checking this focus after your next tracked session.";
|
|
152
|
+
if (isNewFocus) {
|
|
153
|
+
summary = `New focus: ${currentFocus.title}.`;
|
|
154
|
+
}
|
|
155
|
+
else if (currentFocus.lastCheck === "not_followed") {
|
|
156
|
+
summary = `Last session pulled away from ${currentFocus.title.toLowerCase()}.`;
|
|
157
|
+
}
|
|
158
|
+
else if (currentFocus.lastCheck === "followed") {
|
|
159
|
+
summary = `${currentFocus.title} showed up in your last session.`;
|
|
160
|
+
}
|
|
161
|
+
else if (previousState?.currentFocus.title === currentFocus.title) {
|
|
162
|
+
summary =
|
|
163
|
+
"Still active. Temet is calibrating this focus from your next tracked sessions.";
|
|
164
|
+
}
|
|
165
|
+
const notificationBody = [
|
|
166
|
+
summary,
|
|
167
|
+
watchLine,
|
|
168
|
+
firstMeaningful ? `New signal: ${newSignal}.` : null,
|
|
169
|
+
]
|
|
170
|
+
.filter(Boolean)
|
|
171
|
+
.join(" ")
|
|
172
|
+
.replace(/\s+/g, " ")
|
|
173
|
+
.trim();
|
|
174
|
+
return {
|
|
175
|
+
summary,
|
|
176
|
+
reinforced,
|
|
177
|
+
watch: watchLine,
|
|
178
|
+
new: newSignal,
|
|
179
|
+
next,
|
|
180
|
+
notificationTitle: "Temet update",
|
|
181
|
+
notificationBody,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export function buildMenubarState(input) {
|
|
185
|
+
const snapshot = input.tracking?.current ??
|
|
186
|
+
buildAuditSnapshot(input.sourcePath, input.result, input.competencies);
|
|
187
|
+
const report = buildSkillAuditReport(input.result, input.competencies, input.tracking);
|
|
188
|
+
const watchSection = report.blindSpots[0] ??
|
|
189
|
+
{
|
|
190
|
+
title: "No watch item computed yet",
|
|
191
|
+
body: "Run more tracked audits to surface a stable watch item.",
|
|
192
|
+
};
|
|
193
|
+
const topCompetency = pickTopCompetency(input.competencies);
|
|
194
|
+
const watch = {
|
|
195
|
+
title: watchSection.title,
|
|
196
|
+
body: sanitizeLine(watchSection.body, "No watch item computed yet."),
|
|
197
|
+
};
|
|
198
|
+
const currentFocus = evaluateCurrentFocus(watch, snapshot.createdAt, input.previousState?.projectKey === snapshot.projectKey
|
|
199
|
+
? input.previousState
|
|
200
|
+
: null);
|
|
201
|
+
const progress = buildProgress(currentFocus, watch, input.tracking?.changes ?? [], input.previousState?.projectKey === snapshot.projectKey
|
|
202
|
+
? input.previousState
|
|
203
|
+
: null);
|
|
204
|
+
const latestMeaningfulChange = input.tracking?.changes.find((change) => change.type !== "baseline");
|
|
205
|
+
return {
|
|
206
|
+
version: 1,
|
|
207
|
+
projectKey: snapshot.projectKey,
|
|
208
|
+
projectLabel: snapshot.projectLabel,
|
|
209
|
+
createdAt: snapshot.createdAt,
|
|
210
|
+
trackingInstalled: input.trackingInstalled,
|
|
211
|
+
metrics: {
|
|
212
|
+
sessions: snapshot.sessions,
|
|
213
|
+
prompts: input.result.promptCount,
|
|
214
|
+
toolCalls: snapshot.toolCalls,
|
|
215
|
+
workflows: input.result.workflowCount,
|
|
216
|
+
},
|
|
217
|
+
dna: sanitizeLine(input.bilan?.trim() || report.professionalDna, "No professional DNA available yet."),
|
|
218
|
+
superpower: {
|
|
219
|
+
title: topCompetency?.name ??
|
|
220
|
+
report.tacitSkills[0]?.title ??
|
|
221
|
+
"No superpower yet",
|
|
222
|
+
level: topCompetency?.proficiencyLevel ?? "novice",
|
|
223
|
+
evidenceCount: topCompetency ? evidenceCount(topCompetency) : 0,
|
|
224
|
+
},
|
|
225
|
+
watch,
|
|
226
|
+
progress,
|
|
227
|
+
recentChange: latestMeaningfulChange
|
|
228
|
+
? humanizeChange(latestMeaningfulChange)
|
|
229
|
+
: "No new signal surfaced yet.",
|
|
230
|
+
currentFocus,
|
|
231
|
+
lastReportPath: input.lastReportPath ?? null,
|
|
232
|
+
sourcePath: snapshot.sourcePath,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
export async function loadMenubarState() {
|
|
236
|
+
try {
|
|
237
|
+
const raw = await readFile(getMenubarStatePath(), "utf8");
|
|
238
|
+
const parsed = JSON.parse(raw);
|
|
239
|
+
return parsed?.version === 1 ? parsed : null;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
export async function writeMenubarState(input) {
|
|
246
|
+
const state = buildMenubarState(input);
|
|
247
|
+
await mkdir(getMenubarRoot(), { recursive: true });
|
|
248
|
+
await writeFile(getMenubarStatePath(), `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
249
|
+
return state;
|
|
250
|
+
}
|
|
251
|
+
export async function loadMenubarConfig() {
|
|
252
|
+
try {
|
|
253
|
+
const raw = await readFile(getMenubarConfigPath(), "utf8");
|
|
254
|
+
const parsed = JSON.parse(raw);
|
|
255
|
+
return parsed?.version === 1 && parsed.temetBinary ? parsed : null;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
export async function writeMenubarConfig(temetBinary) {
|
|
262
|
+
await mkdir(getMenubarRoot(), { recursive: true });
|
|
263
|
+
const payload = {
|
|
264
|
+
version: 1,
|
|
265
|
+
temetBinary,
|
|
266
|
+
};
|
|
267
|
+
await writeFile(getMenubarConfigPath(), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
268
|
+
return payload;
|
|
269
|
+
}
|
|
270
|
+
export function isMenubarConfigPresent() {
|
|
271
|
+
return existsSync(getMenubarConfigPath());
|
|
272
|
+
}
|
|
273
|
+
export async function resolveLastReportPath(projectLabel) {
|
|
274
|
+
return findLatestReportPath(projectLabel);
|
|
275
|
+
}
|