@temet/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.d.ts +32 -0
- package/dist/audit.js +460 -0
- package/dist/index.js +93 -40
- package/dist/lib/analysis-types.d.ts +7 -0
- package/dist/lib/analysis-types.js +1 -0
- package/dist/lib/audit-tracking.d.ts +55 -0
- package/dist/lib/audit-tracking.js +185 -0
- package/dist/lib/cli-args.d.ts +21 -0
- package/dist/lib/cli-args.js +60 -0
- package/dist/lib/heuristics.d.ts +15 -0
- package/dist/lib/heuristics.js +341 -0
- package/dist/lib/hook-installer.d.ts +13 -0
- package/dist/lib/hook-installer.js +130 -0
- package/dist/lib/narrator-lite.d.ts +25 -0
- package/dist/lib/narrator-lite.js +231 -0
- package/dist/lib/notifier.d.ts +9 -0
- package/dist/lib/notifier.js +57 -0
- package/dist/lib/path-resolver.d.ts +18 -0
- package/dist/lib/path-resolver.js +56 -0
- package/dist/lib/session-audit.d.ts +23 -0
- package/dist/lib/session-audit.js +92 -0
- package/dist/lib/session-parser.d.ts +35 -0
- package/dist/lib/session-parser.js +130 -0
- package/dist/lib/skill-mapper.d.ts +3 -0
- package/dist/lib/skill-mapper.js +173 -0
- package/dist/lib/skill-naming.d.ts +1 -0
- package/dist/lib/skill-naming.js +50 -0
- package/dist/lib/types.d.ts +17 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/workflow-detector.d.ts +11 -0
- package/dist/lib/workflow-detector.js +125 -0
- package/package.json +2 -2
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type AuditResult } from "./lib/session-audit.js";
|
|
2
|
+
import { type AuditChange, type TrackingResult } from "./lib/audit-tracking.js";
|
|
3
|
+
import type { CompetencyEntry } from "./lib/types.js";
|
|
4
|
+
export type AuditOptions = {
|
|
5
|
+
path: string;
|
|
6
|
+
track: boolean;
|
|
7
|
+
narrate: boolean;
|
|
8
|
+
json: boolean;
|
|
9
|
+
quiet: boolean;
|
|
10
|
+
notify: boolean;
|
|
11
|
+
publish: boolean;
|
|
12
|
+
yes: boolean;
|
|
13
|
+
model: string;
|
|
14
|
+
address: string;
|
|
15
|
+
token: string;
|
|
16
|
+
relayUrl: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function buildAuditJsonOutput(result: Pick<AuditResult, "sessionCount" | "messageCount" | "toolCallCount" | "workflows">, competencies: CompetencyEntry[], bilan?: string, tracking?: Pick<TrackingResult, "changes" | "latestPath">): {
|
|
19
|
+
tracking?: {
|
|
20
|
+
latestPath: string;
|
|
21
|
+
changes: AuditChange[];
|
|
22
|
+
} | undefined;
|
|
23
|
+
bilan?: string | undefined;
|
|
24
|
+
sessions: number;
|
|
25
|
+
messages: number;
|
|
26
|
+
toolCalls: number;
|
|
27
|
+
competencies: CompetencyEntry[];
|
|
28
|
+
workflows: import("./lib/workflow-detector.js").DetectedWorkflow[];
|
|
29
|
+
};
|
|
30
|
+
export declare function resolvePublishMode(yes: boolean, isTTY: boolean): "skip" | "reject" | "prompt";
|
|
31
|
+
export declare function hasPublishCredentials(address: string, token: string): boolean;
|
|
32
|
+
export declare function runAuditCommand(opts: AuditOptions): Promise<void>;
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* temet audit — analyze your coding sessions, surface your real skills.
|
|
3
|
+
*/
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
import { clearLine, createInterface, cursorTo } from "node:readline";
|
|
6
|
+
import { findSessionFiles, runAudit, } from "./lib/session-audit.js";
|
|
7
|
+
import { trackAuditSnapshot, } from "./lib/audit-tracking.js";
|
|
8
|
+
export function buildAuditJsonOutput(result, competencies, bilan, tracking) {
|
|
9
|
+
return {
|
|
10
|
+
sessions: result.sessionCount,
|
|
11
|
+
messages: result.messageCount,
|
|
12
|
+
toolCalls: result.toolCallCount,
|
|
13
|
+
competencies,
|
|
14
|
+
workflows: result.workflows,
|
|
15
|
+
...(bilan ? { bilan } : {}),
|
|
16
|
+
...(tracking
|
|
17
|
+
? {
|
|
18
|
+
tracking: {
|
|
19
|
+
latestPath: tracking.latestPath,
|
|
20
|
+
changes: tracking.changes,
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
: {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// ---------- Pretty Output ----------
|
|
27
|
+
const BOLD = "\x1b[1m";
|
|
28
|
+
const DIM = "\x1b[2m";
|
|
29
|
+
const RESET = "\x1b[0m";
|
|
30
|
+
const GREEN = "\x1b[32m";
|
|
31
|
+
const YELLOW = "\x1b[33m";
|
|
32
|
+
const CYAN = "\x1b[36m";
|
|
33
|
+
const MAGENTA = "\x1b[35m";
|
|
34
|
+
const BLUE = "\x1b[34m";
|
|
35
|
+
const RED = "\x1b[31m";
|
|
36
|
+
const SPINNER_FRAMES = ["-", "\\", "|", "/"];
|
|
37
|
+
const TEMET_ASCII = [
|
|
38
|
+
"████████╗███████╗███╗ ███╗███████╗████████╗",
|
|
39
|
+
"╚══██╔══╝██╔════╝████╗ ████║██╔════╝╚══██╔══╝",
|
|
40
|
+
" ██║ █████╗ ██╔████╔██║█████╗ ██║ ",
|
|
41
|
+
" ██║ ██╔══╝ ██║╚██╔╝██║██╔══╝ ██║ ",
|
|
42
|
+
" ██║ ███████╗██║ ╚═╝ ██║███████╗ ██║ ",
|
|
43
|
+
" ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ",
|
|
44
|
+
];
|
|
45
|
+
function useAnsi(stream) {
|
|
46
|
+
return Boolean(stream.isTTY && !process.env.NO_COLOR);
|
|
47
|
+
}
|
|
48
|
+
function colorize(text, color, stream) {
|
|
49
|
+
if (!useAnsi(stream))
|
|
50
|
+
return text;
|
|
51
|
+
return `${color}${text}${RESET}`;
|
|
52
|
+
}
|
|
53
|
+
function dim(text, stream) {
|
|
54
|
+
if (!useAnsi(stream))
|
|
55
|
+
return text;
|
|
56
|
+
return `${DIM}${text}${RESET}`;
|
|
57
|
+
}
|
|
58
|
+
function ok(text, stream) {
|
|
59
|
+
return colorize(text, GREEN, stream);
|
|
60
|
+
}
|
|
61
|
+
function warn(text, stream) {
|
|
62
|
+
return colorize(text, YELLOW, stream);
|
|
63
|
+
}
|
|
64
|
+
function fail(text, stream) {
|
|
65
|
+
return colorize(text, RED, stream);
|
|
66
|
+
}
|
|
67
|
+
function clearCurrentLine(stream) {
|
|
68
|
+
if (!stream.isTTY)
|
|
69
|
+
return;
|
|
70
|
+
clearLine(stream, 0);
|
|
71
|
+
cursorTo(stream, 0);
|
|
72
|
+
}
|
|
73
|
+
function stepPrefix(step, totalSteps, stream) {
|
|
74
|
+
return colorize(`[${step}/${totalSteps}]`, BLUE, stream);
|
|
75
|
+
}
|
|
76
|
+
function formatCount(value) {
|
|
77
|
+
return value.toLocaleString("en-US");
|
|
78
|
+
}
|
|
79
|
+
function formatMs(ms) {
|
|
80
|
+
if (ms < 1000)
|
|
81
|
+
return `${ms}ms`;
|
|
82
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
83
|
+
}
|
|
84
|
+
function printRule(stream) {
|
|
85
|
+
stream.write(`${dim("─".repeat(58), stream)}\n`);
|
|
86
|
+
}
|
|
87
|
+
function printWarningBox(message) {
|
|
88
|
+
const stream = process.stderr;
|
|
89
|
+
printRule(stream);
|
|
90
|
+
stream.write(`${warn("warning", stream)} ${message}\n`);
|
|
91
|
+
printRule(stream);
|
|
92
|
+
}
|
|
93
|
+
function printBanner() {
|
|
94
|
+
const stream = process.stderr;
|
|
95
|
+
if (!stream.isTTY)
|
|
96
|
+
return;
|
|
97
|
+
for (const line of TEMET_ASCII) {
|
|
98
|
+
stream.write(`${colorize(line, CYAN, stream)}\n`);
|
|
99
|
+
}
|
|
100
|
+
stream.write(`\n${BOLD}Temet Audit${RESET}\n`);
|
|
101
|
+
printRule(stream);
|
|
102
|
+
}
|
|
103
|
+
function renderBar(current, total, width = 18) {
|
|
104
|
+
if (total <= 0)
|
|
105
|
+
return `[${"-".repeat(width)}]`;
|
|
106
|
+
const ratio = Math.max(0, Math.min(1, current / total));
|
|
107
|
+
const filled = Math.round(ratio * width);
|
|
108
|
+
return `${"█".repeat(filled)}${"░".repeat(width - filled)}`;
|
|
109
|
+
}
|
|
110
|
+
function truncateLabel(label, max = 28) {
|
|
111
|
+
if (label.length <= max)
|
|
112
|
+
return label;
|
|
113
|
+
return `${label.slice(0, max - 3)}...`;
|
|
114
|
+
}
|
|
115
|
+
function writeStepProgress(step, totalSteps, label, current, total, detail) {
|
|
116
|
+
const stream = process.stderr;
|
|
117
|
+
const percent = total > 0 ? `${Math.round((current / total) * 100)}%` : "0%";
|
|
118
|
+
const parts = [
|
|
119
|
+
stepPrefix(step, totalSteps, stream),
|
|
120
|
+
label,
|
|
121
|
+
dim(`${current}/${total}`, stream),
|
|
122
|
+
colorize(renderBar(current, total), CYAN, stream),
|
|
123
|
+
dim(percent, stream),
|
|
124
|
+
];
|
|
125
|
+
if (detail) {
|
|
126
|
+
parts.push(dim(truncateLabel(detail), stream));
|
|
127
|
+
}
|
|
128
|
+
const line = parts.join(" ");
|
|
129
|
+
if (stream.isTTY) {
|
|
130
|
+
clearCurrentLine(stream);
|
|
131
|
+
stream.write(line);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
stream.write(`${line}\n`);
|
|
135
|
+
}
|
|
136
|
+
function writeStepDone(step, totalSteps, label, detail, elapsedMs) {
|
|
137
|
+
const stream = process.stderr;
|
|
138
|
+
const paddedLabel = label.padEnd(22);
|
|
139
|
+
const parts = [
|
|
140
|
+
stepPrefix(step, totalSteps, stream),
|
|
141
|
+
paddedLabel,
|
|
142
|
+
ok("done", stream),
|
|
143
|
+
];
|
|
144
|
+
if (detail) {
|
|
145
|
+
parts.push(dim(detail, stream));
|
|
146
|
+
}
|
|
147
|
+
if (typeof elapsedMs === "number") {
|
|
148
|
+
parts.push(dim(`in ${formatMs(elapsedMs)}`, stream));
|
|
149
|
+
}
|
|
150
|
+
const line = parts.join(" ");
|
|
151
|
+
if (stream.isTTY) {
|
|
152
|
+
clearCurrentLine(stream);
|
|
153
|
+
}
|
|
154
|
+
stream.write(`${line}\n`);
|
|
155
|
+
}
|
|
156
|
+
async function withSpinner(step, totalSteps, label, task) {
|
|
157
|
+
const stream = process.stderr;
|
|
158
|
+
const startedAt = Date.now();
|
|
159
|
+
if (!stream.isTTY) {
|
|
160
|
+
stream.write(`${stepPrefix(step, totalSteps, stream)} ${label}...\n`);
|
|
161
|
+
const result = await task();
|
|
162
|
+
writeStepDone(step, totalSteps, label, undefined, Date.now() - startedAt);
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
let frameIndex = 0;
|
|
166
|
+
const timer = setInterval(() => {
|
|
167
|
+
clearCurrentLine(stream);
|
|
168
|
+
const frame = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length];
|
|
169
|
+
frameIndex += 1;
|
|
170
|
+
stream.write(`${stepPrefix(step, totalSteps, stream)} ${label} ${colorize(frame, CYAN, stream)}`);
|
|
171
|
+
}, 80);
|
|
172
|
+
try {
|
|
173
|
+
const result = await task();
|
|
174
|
+
clearInterval(timer);
|
|
175
|
+
writeStepDone(step, totalSteps, label, undefined, Date.now() - startedAt);
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
clearInterval(timer);
|
|
180
|
+
clearCurrentLine(stream);
|
|
181
|
+
stream.write(`${stepPrefix(step, totalSteps, stream)} ${label.padEnd(22)} ${fail("failed", stream)}\n`);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const PROF_COLOR = {
|
|
186
|
+
expert: "\x1b[32m",
|
|
187
|
+
proficient: "\x1b[33m",
|
|
188
|
+
competent: "\x1b[36m",
|
|
189
|
+
advanced_beginner: "\x1b[34m",
|
|
190
|
+
novice: "\x1b[37m",
|
|
191
|
+
};
|
|
192
|
+
function profLabel(level) {
|
|
193
|
+
const color = PROF_COLOR[level] ?? "";
|
|
194
|
+
return `${color}${level.replace("_", " ")}${RESET}`;
|
|
195
|
+
}
|
|
196
|
+
function printPretty(result, competencies, bilan, tracking) {
|
|
197
|
+
console.log("");
|
|
198
|
+
console.log(`${dim(`${formatCount(result.sessionCount)} sessions · ${formatCount(result.toolCallCount)} tool calls · ${formatCount(competencies.length)} skills · ${formatCount(result.workflowCount)} workflows`, process.stdout)}`);
|
|
199
|
+
console.log("");
|
|
200
|
+
// Top skills
|
|
201
|
+
const sorted = [...competencies].sort((a, b) => {
|
|
202
|
+
const order = [
|
|
203
|
+
"expert",
|
|
204
|
+
"proficient",
|
|
205
|
+
"competent",
|
|
206
|
+
"advanced_beginner",
|
|
207
|
+
"novice",
|
|
208
|
+
];
|
|
209
|
+
return (order.indexOf(a.proficiencyLevel) - order.indexOf(b.proficiencyLevel));
|
|
210
|
+
});
|
|
211
|
+
const topSkills = sorted.slice(0, 8);
|
|
212
|
+
console.log(`${BOLD}${GREEN}Skills you already demonstrate${RESET}`);
|
|
213
|
+
for (const c of topSkills) {
|
|
214
|
+
console.log(` ${profLabel(c.proficiencyLevel)} ${c.name}`);
|
|
215
|
+
}
|
|
216
|
+
console.log("");
|
|
217
|
+
// Repeated patterns
|
|
218
|
+
console.log(`${BOLD}${CYAN}Repeated patterns${RESET}`);
|
|
219
|
+
if (result.workflows.length > 0) {
|
|
220
|
+
for (const w of result.workflows.slice(0, 5)) {
|
|
221
|
+
console.log(` ${w.description} ${DIM}(${w.occurrences}x across ${w.sessions} sessions)${RESET}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
console.log(` ${DIM}0 workflows detected${RESET}`);
|
|
226
|
+
}
|
|
227
|
+
console.log("");
|
|
228
|
+
// Judgment signals
|
|
229
|
+
const judgmentSignals = result.signals.filter((s) => s.type === "correction" || s.type === "decision_language");
|
|
230
|
+
if (judgmentSignals.length > 0) {
|
|
231
|
+
console.log(`${BOLD}${MAGENTA}Your decisions${RESET}`);
|
|
232
|
+
const seen = new Set();
|
|
233
|
+
for (const s of judgmentSignals) {
|
|
234
|
+
const key = s.skill;
|
|
235
|
+
if (seen.has(key))
|
|
236
|
+
continue;
|
|
237
|
+
seen.add(key);
|
|
238
|
+
if (seen.size > 5)
|
|
239
|
+
break;
|
|
240
|
+
const verb = s.type === "correction" ? "You correct for" : "You prioritize";
|
|
241
|
+
console.log(` ${verb} ${s.skill}`);
|
|
242
|
+
}
|
|
243
|
+
console.log("");
|
|
244
|
+
}
|
|
245
|
+
if (tracking) {
|
|
246
|
+
console.log(`${BOLD}${BLUE}Meaningful changes${RESET}`);
|
|
247
|
+
if (tracking.changes.length > 0) {
|
|
248
|
+
for (const change of tracking.changes) {
|
|
249
|
+
const marker = change.type === "level_down"
|
|
250
|
+
? warn("↓", process.stdout)
|
|
251
|
+
: change.type === "baseline"
|
|
252
|
+
? colorize("•", CYAN, process.stdout)
|
|
253
|
+
: ok("↑", process.stdout);
|
|
254
|
+
console.log(` ${marker} ${change.title}`);
|
|
255
|
+
console.log(` ${DIM}${change.detail}${RESET}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
console.log(` ${DIM}No meaningful changes since the last tracked audit${RESET}`);
|
|
260
|
+
}
|
|
261
|
+
console.log("");
|
|
262
|
+
}
|
|
263
|
+
// Bilan from narrator
|
|
264
|
+
if (bilan) {
|
|
265
|
+
console.log(`${BOLD}${YELLOW}Profile summary${RESET}`);
|
|
266
|
+
console.log("");
|
|
267
|
+
console.log(bilan);
|
|
268
|
+
console.log("");
|
|
269
|
+
}
|
|
270
|
+
// Next actions
|
|
271
|
+
console.log(`${dim("─".repeat(44), process.stdout)}`);
|
|
272
|
+
console.log(`${BOLD}Next${RESET}`);
|
|
273
|
+
if (!tracking) {
|
|
274
|
+
console.log(` ${DIM}temet audit --path <dir> --track${RESET} Save a baseline and track changes`);
|
|
275
|
+
}
|
|
276
|
+
console.log(` ${DIM}temet audit --path <dir> --json${RESET} Export as JSON`);
|
|
277
|
+
console.log(` ${DIM}temet audit --path <dir> --publish${RESET} Publish to your Temet card`);
|
|
278
|
+
console.log("");
|
|
279
|
+
console.log(`${ok("Audit complete:", process.stdout)} ${formatCount(competencies.length)} skills surfaced from ${formatCount(result.sessionCount)} sessions`);
|
|
280
|
+
if (tracking) {
|
|
281
|
+
console.log(`${dim(`Snapshot saved to ${tracking.latestPath}`, process.stdout)}`);
|
|
282
|
+
}
|
|
283
|
+
console.log("");
|
|
284
|
+
}
|
|
285
|
+
// ---------- Confirmation ----------
|
|
286
|
+
async function confirmPublish(competencyCount, address) {
|
|
287
|
+
// Non-interactive (piped stdin) → refuse without --yes
|
|
288
|
+
if (!process.stdin.isTTY) {
|
|
289
|
+
console.error("[temet] non-interactive mode: use --yes to confirm publish");
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
293
|
+
return new Promise((resolve) => {
|
|
294
|
+
rl.question(`\n${BOLD}Publish ${competencyCount} competencies to card ${address}?${RESET} [y/N] `, (answer) => {
|
|
295
|
+
rl.close();
|
|
296
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
export function resolvePublishMode(yes, isTTY) {
|
|
301
|
+
if (yes)
|
|
302
|
+
return "skip";
|
|
303
|
+
if (!isTTY)
|
|
304
|
+
return "reject";
|
|
305
|
+
return "prompt";
|
|
306
|
+
}
|
|
307
|
+
export function hasPublishCredentials(address, token) {
|
|
308
|
+
return Boolean(address && token);
|
|
309
|
+
}
|
|
310
|
+
// ---------- Publish ----------
|
|
311
|
+
async function publishCompetencies(competencies, opts) {
|
|
312
|
+
if (!hasPublishCredentials(opts.address, opts.token)) {
|
|
313
|
+
console.error("[temet] --publish requires TEMET_ADDRESS and TEMET_TOKEN env vars");
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
const url = `${opts.relayUrl}/ingest/${opts.address}`;
|
|
317
|
+
console.error(`[temet] publishing ${competencies.length} competencies to ${url}`);
|
|
318
|
+
const resp = await fetch(url, {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: {
|
|
321
|
+
"Content-Type": "application/json",
|
|
322
|
+
Authorization: `Bearer ${opts.token}`,
|
|
323
|
+
},
|
|
324
|
+
body: JSON.stringify({ competencies }),
|
|
325
|
+
});
|
|
326
|
+
if (!resp.ok) {
|
|
327
|
+
const body = await resp.text();
|
|
328
|
+
console.error(`[temet] publish failed (${resp.status}): ${body}`);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
const result = (await resp.json());
|
|
332
|
+
console.log("");
|
|
333
|
+
console.log(`${BOLD}${GREEN}Published!${RESET}`);
|
|
334
|
+
console.log(` Skills: ${result.totalSkills}`);
|
|
335
|
+
console.log(` Card version: ${result.cardVersion}`);
|
|
336
|
+
console.log(` View: https://temetapp.com/a/${opts.address}`);
|
|
337
|
+
console.log("");
|
|
338
|
+
}
|
|
339
|
+
// ---------- Main ----------
|
|
340
|
+
export async function runAuditCommand(opts) {
|
|
341
|
+
const sessionFiles = findSessionFiles(opts.path);
|
|
342
|
+
if (sessionFiles.length === 0) {
|
|
343
|
+
if (!opts.quiet) {
|
|
344
|
+
console.error(`[temet] no .jsonl session files found in ${opts.path}`);
|
|
345
|
+
}
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
if (!opts.json && !opts.quiet) {
|
|
349
|
+
printBanner();
|
|
350
|
+
}
|
|
351
|
+
if (!opts.quiet) {
|
|
352
|
+
console.error(`[temet] scanning ${sessionFiles.length} session file(s)...`);
|
|
353
|
+
}
|
|
354
|
+
const totalSteps = 3 + (opts.narrate ? 1 : 0) + (opts.publish ? 1 : 0);
|
|
355
|
+
let completedFiles = 0;
|
|
356
|
+
const commandStartedAt = Date.now();
|
|
357
|
+
if (!opts.quiet) {
|
|
358
|
+
writeStepProgress(1, totalSteps, "Scanning sessions", 0, sessionFiles.length);
|
|
359
|
+
}
|
|
360
|
+
let scanDoneWritten = false;
|
|
361
|
+
const result = await runAudit(sessionFiles, (event) => {
|
|
362
|
+
if (opts.quiet)
|
|
363
|
+
return;
|
|
364
|
+
if (event.phase === "scan") {
|
|
365
|
+
completedFiles += 1;
|
|
366
|
+
writeStepProgress(1, totalSteps, "Scanning sessions", completedFiles, sessionFiles.length, event.file ? basename(event.file) : undefined);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (!scanDoneWritten) {
|
|
370
|
+
scanDoneWritten = true;
|
|
371
|
+
writeStepDone(1, totalSteps, "Scanning sessions", `${sessionFiles.length} files`);
|
|
372
|
+
}
|
|
373
|
+
if (event.phase === "signals") {
|
|
374
|
+
writeStepDone(2, totalSteps, "Extracting signals", `${formatCount(event.current ?? 0)} signals`, event.elapsedMs);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (event.phase === "patterns") {
|
|
378
|
+
writeStepDone(3, totalSteps, "Detecting patterns", `${formatCount(event.current ?? 0)} workflows`, event.elapsedMs);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
let { competencies } = result;
|
|
382
|
+
let bilan;
|
|
383
|
+
let tracking;
|
|
384
|
+
// Narrator enrichment
|
|
385
|
+
if (opts.narrate && !opts.quiet) {
|
|
386
|
+
try {
|
|
387
|
+
const narratorModule = await import("./lib/narrator-lite.js");
|
|
388
|
+
if (!narratorModule.resolveAuth()) {
|
|
389
|
+
printWarningBox("--narrate requires model access. Set ANTHROPIC_API_KEY or CLAUDE_AUTH_TOKEN.");
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
const narrated = await withSpinner(4, totalSteps, "Narrating profile", async () => narratorModule.narrateCompetencies(competencies, result.combined, result.workflows, { model: opts.model || undefined }));
|
|
393
|
+
competencies = narrated.competencies;
|
|
394
|
+
bilan = narrated.bilan || undefined;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
console.error("[temet] narrator failed, using heuristic results:", err);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (opts.track) {
|
|
402
|
+
tracking = await trackAuditSnapshot(opts.path, result, competencies);
|
|
403
|
+
// OS notification (only behind --notify, and only if something changed)
|
|
404
|
+
if (opts.notify && !tracking.skipped && tracking.changes.length > 0) {
|
|
405
|
+
const { formatNotification, sendNotification } = await import("./lib/notifier.js");
|
|
406
|
+
const payload = formatNotification(tracking.changes, tracking.current.projectLabel);
|
|
407
|
+
if (payload) {
|
|
408
|
+
void sendNotification(payload).catch(() => { });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// Publish (runs even in quiet mode — it's a side-effect, not output)
|
|
413
|
+
if (opts.publish) {
|
|
414
|
+
const publishMode = resolvePublishMode(opts.yes, Boolean(process.stdin.isTTY));
|
|
415
|
+
if (publishMode === "reject") {
|
|
416
|
+
if (!opts.quiet) {
|
|
417
|
+
printWarningBox("non-interactive mode: use --yes to confirm publish");
|
|
418
|
+
}
|
|
419
|
+
console.error("[temet] publish requires --yes in non-interactive mode");
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
const confirmed = publishMode === "skip" ||
|
|
424
|
+
(await confirmPublish(competencies.length, opts.address));
|
|
425
|
+
if (confirmed) {
|
|
426
|
+
if (opts.quiet) {
|
|
427
|
+
await publishCompetencies(competencies, {
|
|
428
|
+
address: opts.address,
|
|
429
|
+
token: opts.token,
|
|
430
|
+
relayUrl: opts.relayUrl,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
await withSpinner(totalSteps, totalSteps, "Publishing card", () => publishCompetencies(competencies, {
|
|
435
|
+
address: opts.address,
|
|
436
|
+
token: opts.token,
|
|
437
|
+
relayUrl: opts.relayUrl,
|
|
438
|
+
}));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else if (!opts.quiet) {
|
|
442
|
+
console.error("[temet] publish cancelled");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Quiet mode: no output
|
|
447
|
+
if (opts.quiet)
|
|
448
|
+
return;
|
|
449
|
+
// Output
|
|
450
|
+
if (opts.json) {
|
|
451
|
+
const output = buildAuditJsonOutput(result, competencies, bilan, tracking);
|
|
452
|
+
console.log(JSON.stringify(output, null, 2));
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
printPretty(result, competencies, bilan, tracking);
|
|
456
|
+
}
|
|
457
|
+
if (!opts.json) {
|
|
458
|
+
console.error(`${ok("done", process.stderr)} ${dim(`in ${formatMs(Date.now() - commandStartedAt)}`, process.stderr)}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,25 +4,42 @@ 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";
|
|
8
|
+
import { resolveSessionPath } from "./lib/path-resolver.js";
|
|
7
9
|
const execFileAsync = promisify(execFile);
|
|
8
10
|
const DEFAULT_RELAY_URL = "https://temet-relay.ramponneau.workers.dev/mcp";
|
|
9
11
|
const DEFAULT_SERVER_NAME = "temet";
|
|
10
12
|
const DEFAULT_PROTOCOL_APP_NAME = "Temet Handler";
|
|
11
13
|
const LINUX_DESKTOP_ENTRY = "temet-handler.desktop";
|
|
12
14
|
const LINUX_HANDLER_SCRIPT = "temet-protocol-handler";
|
|
13
|
-
const HELP = `Temet CLI
|
|
15
|
+
const HELP = `Temet CLI — discover the skills you already demonstrate in AI work
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
\ttemet
|
|
17
|
-
\ttemet
|
|
18
|
-
\ttemet
|
|
19
|
-
\ttemet
|
|
17
|
+
Commands:
|
|
18
|
+
\ttemet audit [--path <session-dir>] Analyze local sessions, surface skills and workflows
|
|
19
|
+
\ttemet install-hook Auto-audit after every Claude Code session
|
|
20
|
+
\ttemet uninstall-hook Remove the SessionEnd hook
|
|
21
|
+
\ttemet connect --address <hex> --token <t> Connect your MCP client to your Temet card
|
|
22
|
+
\ttemet install-handler Register temet:// protocol handler
|
|
23
|
+
|
|
24
|
+
Audit options:
|
|
25
|
+
\t--path <dir> Directory containing .jsonl session files (auto-detected if omitted)
|
|
26
|
+
\t--track Save a local snapshot and compare against the previous audit
|
|
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
|
|
23
|
-
\ttemet
|
|
24
|
-
\
|
|
25
|
-
\
|
|
38
|
+
\ttemet audit Auto-detect sessions from cwd
|
|
39
|
+
\ttemet audit --path ~/.claude/projects/my-project
|
|
40
|
+
\ttemet audit --path ~/.claude/projects/my-project --track
|
|
41
|
+
\ttemet audit --path ~/.claude/projects/my-project --json
|
|
42
|
+
\ttemet install-hook Background audit on session end
|
|
26
43
|
`;
|
|
27
44
|
function printHelp(exitCode = 0) {
|
|
28
45
|
console.log(HELP);
|
|
@@ -38,36 +55,6 @@ function normalizeAddress(raw) {
|
|
|
38
55
|
}
|
|
39
56
|
return address;
|
|
40
57
|
}
|
|
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
58
|
function parseConnectOptions(flags) {
|
|
72
59
|
const address = normalizeAddress(String(flags.get("address") ?? ""));
|
|
73
60
|
const token = String(flags.get("token") ?? "").trim();
|
|
@@ -156,6 +143,22 @@ function parseArgs(argv) {
|
|
|
156
143
|
const [command, ...rest] = argv;
|
|
157
144
|
const { flags, positionals } = parseFlagBag(rest);
|
|
158
145
|
const dryRun = Boolean(flags.get("dry-run"));
|
|
146
|
+
if (command === "audit") {
|
|
147
|
+
const options = buildAuditCliOptions(flags, process.env);
|
|
148
|
+
if (!options.path) {
|
|
149
|
+
// Auto-detect session path
|
|
150
|
+
const detected = resolveSessionPath(undefined, process.env);
|
|
151
|
+
if (!detected) {
|
|
152
|
+
console.error("[temet] could not auto-detect session directory. Use --path <session-dir>");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
options.path = detected;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
command,
|
|
159
|
+
options,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
159
162
|
if (command === "connect") {
|
|
160
163
|
return {
|
|
161
164
|
command,
|
|
@@ -171,6 +174,9 @@ function parseArgs(argv) {
|
|
|
171
174
|
if (command === "install-handler" || command === "uninstall-handler") {
|
|
172
175
|
return { command, dryRun };
|
|
173
176
|
}
|
|
177
|
+
if (command === "install-hook" || command === "uninstall-hook") {
|
|
178
|
+
return { command, dryRun };
|
|
179
|
+
}
|
|
174
180
|
console.error(`Unknown command: ${command}`);
|
|
175
181
|
printHelp(1);
|
|
176
182
|
}
|
|
@@ -491,10 +497,57 @@ async function uninstallProtocolHandler(dryRun) {
|
|
|
491
497
|
}
|
|
492
498
|
async function run() {
|
|
493
499
|
const parsed = parseArgs(process.argv.slice(2));
|
|
500
|
+
if (parsed.command === "audit") {
|
|
501
|
+
const { runAuditCommand } = await import("./audit.js");
|
|
502
|
+
await runAuditCommand(parsed.options);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
494
505
|
if (parsed.command === "connect" || parsed.command === "connect-url") {
|
|
495
506
|
await runWriteFlow(parsed.command, parsed.options);
|
|
496
507
|
return;
|
|
497
508
|
}
|
|
509
|
+
if (parsed.command === "install-hook") {
|
|
510
|
+
const { resolveTemetBinary, readSettings, writeSettings, isHookInstalled, installHook, getSettingsPath, } = await import("./lib/hook-installer.js");
|
|
511
|
+
const settingsPath = getSettingsPath();
|
|
512
|
+
const settings = readSettings(settingsPath);
|
|
513
|
+
if (isHookInstalled(settings)) {
|
|
514
|
+
console.log("[temet] Hook already installed.");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const binary = resolveTemetBinary();
|
|
518
|
+
if (!binary) {
|
|
519
|
+
console.error("[temet] Could not resolve temet binary. Install @temet/cli globally first: npm i -g @temet/cli");
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
if (parsed.dryRun) {
|
|
523
|
+
console.log(`[temet] dry-run: binary resolved to: ${binary}`);
|
|
524
|
+
console.log(`[temet] dry-run: would write to ${settingsPath}`);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const updated = installHook(settings, binary);
|
|
528
|
+
writeSettings(settingsPath, updated);
|
|
529
|
+
console.log(`[temet] Resolved binary: ${binary}`);
|
|
530
|
+
console.log(`[temet] SessionEnd hook installed in ${settingsPath}`);
|
|
531
|
+
console.log("[temet] Restart Claude Code for the hook to take effect.");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (parsed.command === "uninstall-hook") {
|
|
535
|
+
const { readSettings, writeSettings, isHookInstalled, uninstallHook, getSettingsPath, } = await import("./lib/hook-installer.js");
|
|
536
|
+
const settingsPath = getSettingsPath();
|
|
537
|
+
const settings = readSettings(settingsPath);
|
|
538
|
+
if (!isHookInstalled(settings)) {
|
|
539
|
+
console.log("[temet] No hook installed.");
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (parsed.dryRun) {
|
|
543
|
+
console.log(`[temet] dry-run: would remove hook from ${settingsPath}`);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const updated = uninstallHook(settings);
|
|
547
|
+
writeSettings(settingsPath, updated);
|
|
548
|
+
console.log("[temet] SessionEnd hook removed.");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
498
551
|
if (parsed.command === "install-handler") {
|
|
499
552
|
await installProtocolHandler(parsed.dryRun);
|
|
500
553
|
console.log("[temet] next: click a Quick connect button in Temet.");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|