@temet/cli 0.3.1 → 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.js +147 -30
- package/dist/index.js +56 -0
- package/dist/lib/audit-tracking.js +13 -2
- package/dist/lib/cli-args.js +6 -1
- package/dist/lib/diagnostic-runner.d.ts +13 -0
- package/dist/lib/diagnostic-runner.js +95 -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/report-writer.d.ts +2 -0
- package/dist/lib/report-writer.js +19 -1
- 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,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
|
+
}
|
package/dist/lib/notifier.d.ts
CHANGED
|
@@ -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>;
|
package/dist/lib/notifier.js
CHANGED
|
@@ -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 {};
|
package/dist/plan.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { resolveDiagnostics } from "./lib/diagnostic-runner.js";
|
|
2
|
+
import { buildSkillAuditReport } from "./lib/profile-report.js";
|
|
3
|
+
function actionForBlindSpot(title) {
|
|
4
|
+
switch (title) {
|
|
5
|
+
case "Feature sprawl":
|
|
6
|
+
return {
|
|
7
|
+
title,
|
|
8
|
+
action: "Finish one live path before opening another new direction this week.",
|
|
9
|
+
reason: "Your output is not the constraint. Selection and closure are.",
|
|
10
|
+
};
|
|
11
|
+
case "Git hygiene under pressure":
|
|
12
|
+
return {
|
|
13
|
+
title,
|
|
14
|
+
action: "Reduce branch churn and land one clean, reviewable slice before the next risky change.",
|
|
15
|
+
reason: "The skill is there; the failure mode is pressure, not ignorance.",
|
|
16
|
+
};
|
|
17
|
+
case "Technical detours":
|
|
18
|
+
return {
|
|
19
|
+
title,
|
|
20
|
+
action: "Timebox deep exploration and force a product decision when the learning curve flattens.",
|
|
21
|
+
reason: "Curiosity is helping you, but it still needs a stopping rule.",
|
|
22
|
+
};
|
|
23
|
+
case "Coordination drag":
|
|
24
|
+
return {
|
|
25
|
+
title,
|
|
26
|
+
action: "Write the one-line decision and owner before you expand the system further.",
|
|
27
|
+
reason: "Alignment cost is starting to compete with build cost.",
|
|
28
|
+
};
|
|
29
|
+
default:
|
|
30
|
+
return {
|
|
31
|
+
title,
|
|
32
|
+
action: "Keep the loop short and choose one concrete improvement to test next.",
|
|
33
|
+
reason: "The report points to a leverage point, not a reason to add more layers.",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function buildPlanItems(blindSpots, meaningfulChanges) {
|
|
38
|
+
const items = blindSpots
|
|
39
|
+
.slice(0, 3)
|
|
40
|
+
.map((spot) => actionForBlindSpot(spot.title));
|
|
41
|
+
if (meaningfulChanges.length > 0) {
|
|
42
|
+
items.push({
|
|
43
|
+
title: "Recent movement",
|
|
44
|
+
action: "Protect the newest positive change and turn it into a repeated habit.",
|
|
45
|
+
reason: meaningfulChanges[0] ?? "A new pattern just surfaced; reinforce it.",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return items.slice(0, 3);
|
|
49
|
+
}
|
|
50
|
+
export function buildPlanJsonOutput(input) {
|
|
51
|
+
return input;
|
|
52
|
+
}
|
|
53
|
+
function printPlan(output) {
|
|
54
|
+
console.log("PLAN");
|
|
55
|
+
console.log("");
|
|
56
|
+
console.log(output.professionalDna);
|
|
57
|
+
console.log("");
|
|
58
|
+
console.log("Keep");
|
|
59
|
+
for (const item of output.keep) {
|
|
60
|
+
console.log(` - ${item.title}${item.tagline ? ` — ${item.tagline}` : ""}`);
|
|
61
|
+
}
|
|
62
|
+
console.log("");
|
|
63
|
+
console.log("Watch");
|
|
64
|
+
for (const item of output.watch) {
|
|
65
|
+
console.log(` - ${item.title}: ${item.body}`);
|
|
66
|
+
}
|
|
67
|
+
console.log("");
|
|
68
|
+
console.log("Plan");
|
|
69
|
+
for (const item of output.plan) {
|
|
70
|
+
console.log(` - ${item.action}`);
|
|
71
|
+
console.log(` Why: ${item.reason}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function runPlanCommand(opts) {
|
|
75
|
+
const diagnostics = await resolveDiagnostics(opts);
|
|
76
|
+
if (opts.quiet)
|
|
77
|
+
return;
|
|
78
|
+
const report = buildSkillAuditReport(diagnostics.result, diagnostics.competencies, diagnostics.tracking);
|
|
79
|
+
const output = buildPlanJsonOutput({
|
|
80
|
+
professionalDna: diagnostics.bilan?.trim() || report.professionalDna,
|
|
81
|
+
keep: report.tacitSkills.slice(0, 3),
|
|
82
|
+
watch: report.blindSpots.slice(0, 3),
|
|
83
|
+
plan: buildPlanItems(report.blindSpots, report.meaningfulChanges),
|
|
84
|
+
});
|
|
85
|
+
if (opts.json) {
|
|
86
|
+
console.log(JSON.stringify(output, null, 2));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
printPlan(output);
|
|
90
|
+
}
|
package/dist/traces.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AuditCliOptions } from "./lib/cli-args.js";
|
|
2
|
+
import type { CompetencyEntry } from "./lib/types.js";
|
|
3
|
+
type EvidenceItem = {
|
|
4
|
+
kind: "example" | "decision" | "anti_pattern" | "mentor";
|
|
5
|
+
text: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function buildTracesJsonOutput(input: {
|
|
8
|
+
projectLabel: string;
|
|
9
|
+
sessions: number;
|
|
10
|
+
prompts: number;
|
|
11
|
+
toolCalls: number;
|
|
12
|
+
competencies: CompetencyEntry[];
|
|
13
|
+
workflows: Array<{
|
|
14
|
+
description: string;
|
|
15
|
+
sequence: string[];
|
|
16
|
+
occurrences: number;
|
|
17
|
+
sessions: number;
|
|
18
|
+
confidence: number;
|
|
19
|
+
}>;
|
|
20
|
+
}): {
|
|
21
|
+
project: string;
|
|
22
|
+
sessions: number;
|
|
23
|
+
prompts: number;
|
|
24
|
+
toolCalls: number;
|
|
25
|
+
competencies: {
|
|
26
|
+
name: string;
|
|
27
|
+
category: import("./lib/types.js").CompetencyCategory;
|
|
28
|
+
proficiencyLevel: import("./lib/types.js").ProficiencyLevel;
|
|
29
|
+
description: string;
|
|
30
|
+
evidence: EvidenceItem[];
|
|
31
|
+
}[];
|
|
32
|
+
workflows: {
|
|
33
|
+
description: string;
|
|
34
|
+
sequence: string[];
|
|
35
|
+
occurrences: number;
|
|
36
|
+
sessions: number;
|
|
37
|
+
confidence: number;
|
|
38
|
+
}[];
|
|
39
|
+
};
|
|
40
|
+
export declare function runTracesCommand(opts: AuditCliOptions): Promise<void>;
|
|
41
|
+
export {};
|