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