@temet/cli 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ import { basename } from "node:path";
2
+ import { createInterface } from "node:readline";
3
+ import { findClaudeProjectCandidates, 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
+ let sessionFiles = findSessionFiles(resolvedPath);
59
+ if (sessionFiles.length === 0 && !opts.path && !opts.quiet && !opts.json) {
60
+ const candidates = findClaudeProjectCandidates();
61
+ if (candidates.length > 0) {
62
+ const selected = await chooseSessionCandidate(candidates);
63
+ if (selected) {
64
+ resolvedPath = selected.sessionDir;
65
+ sessionFiles = findSessionFiles(resolvedPath);
66
+ process.stderr.write(`[temet] using ${selected.label}\n`);
67
+ }
68
+ }
69
+ }
70
+ if (sessionFiles.length === 0) {
71
+ throw new Error(`No .jsonl session files found in ${resolvedPath}.`);
72
+ }
73
+ if (!opts.quiet) {
74
+ process.stderr.write(`[temet] reading ${sessionFiles.length} saved session file(s)...\n`);
75
+ }
76
+ const result = await runAudit(sessionFiles);
77
+ let competencies = result.competencies;
78
+ let bilan;
79
+ if (opts.narrate && !opts.quiet) {
80
+ try {
81
+ const narratorModule = await import("./narrator-lite.js");
82
+ if (narratorModule.resolveAuth()) {
83
+ const narrated = await narratorModule.narrateCompetencies(competencies, result.combined, result.workflows, { model: opts.model || undefined });
84
+ competencies = narrated.competencies;
85
+ bilan = narrated.bilan || undefined;
86
+ }
87
+ }
88
+ catch {
89
+ // Keep deterministic fallback if narration fails.
90
+ }
91
+ }
92
+ let tracking;
93
+ if (opts.track) {
94
+ tracking = await trackAuditSnapshot(resolvedPath, result, competencies);
95
+ }
96
+ return {
97
+ resolvedPath,
98
+ result,
99
+ competencies,
100
+ bilan,
101
+ tracking,
102
+ };
103
+ }
104
+ export function defaultProjectLabel(resolvedPath) {
105
+ return basename(resolvedPath);
106
+ }
@@ -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
+ }
@@ -1,9 +1,11 @@
1
1
  import type { AuditChange } from "./audit-tracking.js";
2
+ import type { MenubarProgress } from "./menubar-state.js";
2
3
  export type NotificationPayload = {
3
4
  title: string;
4
5
  body: string;
5
6
  };
6
7
  /** Build the notification text from audit changes. Returns null if nothing to notify. */
7
8
  export declare function formatNotification(changes: AuditChange[], projectLabel: string): NotificationPayload | null;
9
+ export declare function formatProgressNotification(progress: MenubarProgress | null | undefined): NotificationPayload | null;
8
10
  /** Send a native OS notification. Fire-and-forget. */
9
11
  export declare function sendNotification(payload: NotificationPayload): Promise<void>;
@@ -23,6 +23,17 @@ export function formatNotification(changes, projectLabel) {
23
23
  : `${first.title} and ${meaningful.length - 1} more`;
24
24
  return { title: "Temet", body };
25
25
  }
26
+ export function formatProgressNotification(progress) {
27
+ if (!progress)
28
+ return null;
29
+ const body = progress.notificationBody?.trim();
30
+ if (!body)
31
+ return null;
32
+ return {
33
+ title: progress.notificationTitle || "Temet update",
34
+ body,
35
+ };
36
+ }
26
37
  /** Send a native OS notification. Fire-and-forget. */
27
38
  export async function sendNotification(payload) {
28
39
  const { title, body } = payload;
@@ -3,5 +3,7 @@ import type { TrackingResult } from "./audit-tracking.js";
3
3
  import type { CompetencyEntry } from "./types.js";
4
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
5
  export declare function buildReportPath(projectLabel: string, now?: Date): string;
6
+ export declare function getReportDirectory(projectLabel: string): string;
7
+ export declare function findLatestReportPath(projectLabel: string): Promise<string | null>;
6
8
  export declare function saveAuditTextReport(projectLabel: string, content: string, now?: Date): Promise<string>;
7
9
  export declare function openReportFile(filePath: string, platform?: NodeJS.Platform): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
- import { mkdir, writeFile } from "node:fs/promises";
2
+ import { mkdir, readdir, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
  import { promisify } from "node:util";
@@ -54,6 +54,24 @@ export function buildReportPath(projectLabel, now = new Date()) {
54
54
  const safeLabel = projectLabel.replace(/[^a-zA-Z0-9._-]+/g, "-");
55
55
  return path.join(homedir(), ".temet", "reports", safeLabel, `temet-skills-audit-${date}.txt`);
56
56
  }
57
+ export function getReportDirectory(projectLabel) {
58
+ const safeLabel = projectLabel.replace(/[^a-zA-Z0-9._-]+/g, "-");
59
+ return path.join(homedir(), ".temet", "reports", safeLabel);
60
+ }
61
+ export async function findLatestReportPath(projectLabel) {
62
+ const dir = getReportDirectory(projectLabel);
63
+ try {
64
+ const entries = await readdir(dir);
65
+ const latest = entries
66
+ .filter((entry) => entry.endsWith(".txt"))
67
+ .sort()
68
+ .at(-1);
69
+ return latest ? path.join(dir, latest) : null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
57
75
  export async function saveAuditTextReport(projectLabel, content, now = new Date()) {
58
76
  const filePath = buildReportPath(projectLabel, now);
59
77
  await mkdir(path.dirname(filePath), { recursive: true });
package/dist/plan.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import type { AuditCliOptions } from "./lib/cli-args.js";
2
+ type PlanItem = {
3
+ title: string;
4
+ action: string;
5
+ reason: string;
6
+ };
7
+ export declare function buildPlanJsonOutput(input: {
8
+ professionalDna: string;
9
+ keep: Array<{
10
+ title: string;
11
+ tagline?: string;
12
+ body: string;
13
+ }>;
14
+ watch: Array<{
15
+ title: string;
16
+ body: string;
17
+ }>;
18
+ plan: PlanItem[];
19
+ }): {
20
+ professionalDna: string;
21
+ keep: Array<{
22
+ title: string;
23
+ tagline?: string;
24
+ body: string;
25
+ }>;
26
+ watch: Array<{
27
+ title: string;
28
+ body: string;
29
+ }>;
30
+ plan: PlanItem[];
31
+ };
32
+ export declare function runPlanCommand(opts: AuditCliOptions): Promise<void>;
33
+ export {};