@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.
@@ -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
- Usage:
16
- \ttemet connect --address <16hex> --token <token> [--config-path <path>] [--relay-url <url>] [--name temet] [--dry-run]
17
- \ttemet connect-url --url "temet://connect?address=<16hex>&token=<token>&relay=<url>&name=temet" [--config-path <path>] [--name temet] [--relay-url <url>] [--dry-run]
18
- \ttemet install-handler [--dry-run]
19
- \ttemet uninstall-handler [--dry-run]
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 connect --address a3f8c2d19f0b7e41 --token <token>
23
- \ttemet connect-url --url "temet://connect?address=a3f8c2d19f0b7e41&token=<token>&relay=https%3A%2F%2Ftemet-relay.ramponneau.workers.dev%2Fmcp"
24
- \tpnpm dlx @temet/cli install-handler
25
- \tpnpm dlx @temet/cli connect-url --url "temet://connect?address=a3f8c2d19f0b7e41&token=<token>"
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,7 @@
1
+ import type { SessionMessage, ToolCall } from "./session-parser.js";
2
+ export type CombinedStats = {
3
+ messages: SessionMessage[];
4
+ toolCalls: ToolCall[];
5
+ filesTouched: string[];
6
+ allToolCallsBySession: ToolCall[][];
7
+ };
@@ -0,0 +1 @@
1
+ export {};