@temet/cli 0.3.2 → 0.3.4
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.js +90 -35
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +79 -0
- package/dist/index.js +37 -1
- package/dist/lib/diagnostic-runner.js +32 -4
- package/dist/lib/path-resolver.d.ts +8 -0
- package/dist/lib/path-resolver.js +46 -0
- package/dist/lib/report-writer.js +1 -1
- package/dist/lib/session-parser.d.ts +2 -0
- package/dist/lib/session-parser.js +141 -2
- package/package.json +1 -1
package/dist/audit.js
CHANGED
|
@@ -6,7 +6,7 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import { basename, join } from "node:path";
|
|
7
7
|
import { clearLine, createInterface, cursorTo } from "node:readline";
|
|
8
8
|
import { findSessionFiles, runAudit, } from "./lib/session-audit.js";
|
|
9
|
-
import { resolveSessionPath, } from "./lib/path-resolver.js";
|
|
9
|
+
import { findCodexSessionFiles, resolveSessionPath, } from "./lib/path-resolver.js";
|
|
10
10
|
import { trackAuditSnapshot, } from "./lib/audit-tracking.js";
|
|
11
11
|
import { loadMenubarState, resolveLastReportPath, writeMenubarState, } from "./lib/menubar-state.js";
|
|
12
12
|
export function buildAuditJsonOutput(result, competencies, bilan, tracking) {
|
|
@@ -276,11 +276,11 @@ function printPretty(result, competencies, bilan, tracking) {
|
|
|
276
276
|
console.log(`${dim("─".repeat(44), process.stdout)}`);
|
|
277
277
|
console.log(`${BOLD}Next${RESET}`);
|
|
278
278
|
if (!tracking) {
|
|
279
|
-
console.log(` ${DIM}temet audit --
|
|
279
|
+
console.log(` ${DIM}temet audit --track${RESET} Keep learning from every Claude Code session`);
|
|
280
280
|
}
|
|
281
|
-
console.log(` ${DIM}temet traces${RESET}
|
|
282
|
-
console.log(` ${DIM}temet plan${RESET}
|
|
283
|
-
console.log(` ${DIM}temet audit --
|
|
281
|
+
console.log(` ${DIM}temet traces${RESET} See why Temet says this about you`);
|
|
282
|
+
console.log(` ${DIM}temet plan${RESET} Get one focused next step to improve with AI`);
|
|
283
|
+
console.log(` ${DIM}temet audit --publish${RESET} Share this profile on your Temet card`);
|
|
284
284
|
console.log("");
|
|
285
285
|
console.log(`${ok("Audit complete:", process.stdout)} ${formatCount(competencies.length)} skills surfaced from ${formatCount(result.sessionCount)} sessions`);
|
|
286
286
|
if (tracking) {
|
|
@@ -308,6 +308,13 @@ function hasDeclinedHook() {
|
|
|
308
308
|
function saveHookDeclined() {
|
|
309
309
|
writePrefs({ ...readPrefs(), hookDeclined: true });
|
|
310
310
|
}
|
|
311
|
+
function clearHookDeclined() {
|
|
312
|
+
const prefs = readPrefs();
|
|
313
|
+
if (!("hookDeclined" in prefs))
|
|
314
|
+
return;
|
|
315
|
+
delete prefs.hookDeclined;
|
|
316
|
+
writePrefs(prefs);
|
|
317
|
+
}
|
|
311
318
|
function isMacOs() {
|
|
312
319
|
return process.platform === "darwin";
|
|
313
320
|
}
|
|
@@ -321,7 +328,7 @@ async function readHookInstalledSafe() {
|
|
|
321
328
|
}
|
|
322
329
|
}
|
|
323
330
|
// ---------- Confirmation ----------
|
|
324
|
-
async function askYesNo(question, defaultYes, hintOverride) {
|
|
331
|
+
async function askYesNo(question, defaultYes, hintOverride, options) {
|
|
325
332
|
if (!process.stdin.isTTY)
|
|
326
333
|
return false;
|
|
327
334
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -330,7 +337,13 @@ async function askYesNo(question, defaultYes, hintOverride) {
|
|
|
330
337
|
? "Press Enter to continue, or type no to skip:"
|
|
331
338
|
: "Type yes to continue, or press Enter to cancel:");
|
|
332
339
|
return new Promise((resolve) => {
|
|
333
|
-
|
|
340
|
+
const labelLine = options?.label
|
|
341
|
+
? `${colorize("●", CYAN, process.stderr)} ${BOLD}${options.label}${RESET}\n`
|
|
342
|
+
: "";
|
|
343
|
+
const descriptionLine = options?.description
|
|
344
|
+
? `${dim(options.description, process.stderr)}\n`
|
|
345
|
+
: "";
|
|
346
|
+
rl.question(`\n${labelLine}${BOLD}${question}${RESET}\n${descriptionLine}${dim(hint, process.stderr)} `, (answer) => {
|
|
334
347
|
rl.close();
|
|
335
348
|
const trimmed = answer.trim().toLowerCase();
|
|
336
349
|
if (trimmed === "")
|
|
@@ -424,6 +437,9 @@ async function publishCompetencies(competencies, opts) {
|
|
|
424
437
|
}
|
|
425
438
|
// ---------- Main ----------
|
|
426
439
|
export async function runAuditCommand(opts) {
|
|
440
|
+
if (!opts.json && !opts.quiet) {
|
|
441
|
+
printBanner();
|
|
442
|
+
}
|
|
427
443
|
const totalSteps = 4 + (opts.narrate ? 1 : 0) + (opts.publish ? 1 : 0);
|
|
428
444
|
const commandStartedAt = Date.now();
|
|
429
445
|
let resolvedPath = opts.path;
|
|
@@ -459,18 +475,39 @@ export async function runAuditCommand(opts) {
|
|
|
459
475
|
}
|
|
460
476
|
resolvedPath = resolution.path;
|
|
461
477
|
}
|
|
462
|
-
|
|
478
|
+
let sessionFiles = findSessionFiles(resolvedPath);
|
|
479
|
+
if (sessionFiles.length === 0 && !opts.path && !opts.quiet && !opts.json) {
|
|
480
|
+
const { findClaudeProjectCandidates } = await import("./lib/path-resolver.js");
|
|
481
|
+
const candidates = findClaudeProjectCandidates();
|
|
482
|
+
if (candidates.length > 0) {
|
|
483
|
+
const selected = await chooseSessionCandidate(candidates);
|
|
484
|
+
if (selected) {
|
|
485
|
+
resolvedPath = selected.sessionDir;
|
|
486
|
+
sessionFiles = findSessionFiles(resolvedPath);
|
|
487
|
+
process.stderr.write(`${dim(`[temet] using ${selected.label}`, process.stderr)}\n`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// Include all Codex sessions from this machine (user-wide audit)
|
|
492
|
+
const codexFiles = opts.path ? [] : findCodexSessionFiles();
|
|
493
|
+
if (codexFiles.length > 0) {
|
|
494
|
+
sessionFiles = [...sessionFiles, ...codexFiles];
|
|
495
|
+
}
|
|
463
496
|
if (sessionFiles.length === 0) {
|
|
464
497
|
if (!opts.quiet) {
|
|
465
|
-
console.error(
|
|
498
|
+
console.error("[temet] no AI session files found on this machine");
|
|
466
499
|
}
|
|
467
500
|
process.exit(1);
|
|
468
501
|
}
|
|
469
|
-
if (!opts.json && !opts.quiet) {
|
|
470
|
-
printBanner();
|
|
471
|
-
}
|
|
472
502
|
if (!opts.quiet) {
|
|
473
|
-
|
|
503
|
+
const claudeCount = sessionFiles.length - codexFiles.length;
|
|
504
|
+
const sources = [
|
|
505
|
+
claudeCount > 0 ? `${claudeCount} Claude` : "",
|
|
506
|
+
codexFiles.length > 0 ? `${codexFiles.length} Codex` : "",
|
|
507
|
+
]
|
|
508
|
+
.filter(Boolean)
|
|
509
|
+
.join(" + ");
|
|
510
|
+
console.error(`[temet] reading ${sessionFiles.length} session file(s)${sources ? ` (${sources})` : ""}`);
|
|
474
511
|
}
|
|
475
512
|
let completedFiles = 0;
|
|
476
513
|
if (!opts.quiet) {
|
|
@@ -602,39 +639,57 @@ export async function runAuditCommand(opts) {
|
|
|
602
639
|
const hookModule = await import("./lib/hook-installer.js");
|
|
603
640
|
const settingsPath = hookModule.getSettingsPath();
|
|
604
641
|
const settings = hookModule.readSettings(settingsPath);
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
642
|
+
let hookAlreadyInstalled = hookModule.isHookInstalled(settings);
|
|
643
|
+
const { isMenubarInstalled, installMenubar } = isMacOs()
|
|
644
|
+
? await import("./lib/menubar-installer.js")
|
|
645
|
+
: {
|
|
646
|
+
isMenubarInstalled: () => false,
|
|
647
|
+
installMenubar: undefined,
|
|
648
|
+
};
|
|
649
|
+
const menubarAlreadyInstalled = isMacOs() ? isMenubarInstalled() : false;
|
|
650
|
+
if (!hookAlreadyInstalled && !opts.track && !hasDeclinedHook()) {
|
|
651
|
+
const answer = await askYesNo("Want Temet to keep learning from your Claude Code sessions?", true, "Press Enter to continue (or type no to skip):", {
|
|
652
|
+
label: "Continuous learning",
|
|
653
|
+
description: "Temet will watch for meaningful changes after each session and keep your progress up to date.",
|
|
654
|
+
});
|
|
655
|
+
if (answer) {
|
|
656
|
+
const binary = hookModule.resolveTemetBinary();
|
|
657
|
+
if (binary) {
|
|
658
|
+
const updated = hookModule.installHook(settings, binary);
|
|
659
|
+
hookModule.writeSettings(settingsPath, updated);
|
|
660
|
+
hookAlreadyInstalled = true;
|
|
661
|
+
clearHookDeclined();
|
|
662
|
+
console.error(`${ok("✓", process.stderr)} Tracking enabled ${dim("Temet will keep an eye on your progress after each session.", process.stderr)}`);
|
|
619
663
|
}
|
|
620
664
|
}
|
|
665
|
+
else {
|
|
666
|
+
saveHookDeclined();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (!isMacOs()) {
|
|
670
|
+
if (hookAlreadyInstalled || opts.track) {
|
|
671
|
+
console.error(`${ok("✓", process.stderr)} ${dim("Automatic tracking is already on.", process.stderr)}`);
|
|
672
|
+
}
|
|
621
673
|
}
|
|
622
674
|
else {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
675
|
+
if (!menubarAlreadyInstalled && installMenubar) {
|
|
676
|
+
const answer = await askYesNo("Want Temet to stay visible between sessions?", true, "Press Enter to continue (or type no to skip):", {
|
|
677
|
+
label: "Stay visible",
|
|
678
|
+
description: "Keep your current focus, recent progress, and latest signals one glance away in your menu bar.",
|
|
679
|
+
});
|
|
628
680
|
if (answer) {
|
|
629
681
|
const result = await installMenubar({
|
|
630
682
|
ensureHook: !hookAlreadyInstalled,
|
|
631
683
|
});
|
|
632
684
|
const suffix = result.hookInstalled
|
|
633
|
-
? "Temet
|
|
634
|
-
: "Temet
|
|
635
|
-
console.error(`${ok("
|
|
685
|
+
? "Temet is open in your menu bar, and automatic tracking is now on."
|
|
686
|
+
: "Temet is open in your menu bar.";
|
|
687
|
+
console.error(`${ok("✓", process.stderr)} Temet installed ${dim(suffix, process.stderr)}`);
|
|
636
688
|
}
|
|
637
689
|
}
|
|
690
|
+
else if (menubarAlreadyInstalled) {
|
|
691
|
+
console.error(`${ok("✓", process.stderr)} ${dim("Temet is already in your menu bar.", process.stderr)}`);
|
|
692
|
+
}
|
|
638
693
|
}
|
|
639
694
|
}
|
|
640
695
|
catch (error) {
|
package/dist/doctor.d.ts
ADDED
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { findClaudeProjectCandidates, resolveSessionPath, } from "./lib/path-resolver.js";
|
|
4
|
+
import { getSettingsPath, isHookInstalled, readSettings, } from "./lib/hook-installer.js";
|
|
5
|
+
import { getMenubarAppPath, isMenubarInstalled, } from "./lib/menubar-installer.js";
|
|
6
|
+
import { getMenubarConfigPath, getMenubarStatePath, } from "./lib/menubar-state.js";
|
|
7
|
+
import { resolveAuth } from "./lib/narrator-lite.js";
|
|
8
|
+
import { findSessionFiles } from "./lib/session-audit.js";
|
|
9
|
+
function buildCheck(id, label, status, detail) {
|
|
10
|
+
return { id, label, status, detail };
|
|
11
|
+
}
|
|
12
|
+
function iconFor(status) {
|
|
13
|
+
return status === "ok" ? "OK" : "WARN";
|
|
14
|
+
}
|
|
15
|
+
export async function runDoctorCommand(opts) {
|
|
16
|
+
const resolution = resolveSessionPath(opts.path || undefined, process.env);
|
|
17
|
+
const resolvedPath = resolution.path;
|
|
18
|
+
const sessionFiles = resolvedPath ? findSessionFiles(resolvedPath) : [];
|
|
19
|
+
const hookInstalled = isHookInstalled(readSettings(getSettingsPath()));
|
|
20
|
+
const narrationAvailable = resolveAuth() !== null;
|
|
21
|
+
const menubarInstalled = process.platform === "darwin" && isMenubarInstalled();
|
|
22
|
+
const menubarConfigExists = existsSync(getMenubarConfigPath());
|
|
23
|
+
const menubarStateExists = existsSync(getMenubarStatePath());
|
|
24
|
+
const candidates = findClaudeProjectCandidates();
|
|
25
|
+
const checks = [
|
|
26
|
+
buildCheck("sessions-root", "Claude Code projects", candidates.length > 0 ? "ok" : "warn", candidates.length > 0
|
|
27
|
+
? `${candidates.length} project(s) with session history found`
|
|
28
|
+
: "No Claude Code project with session history was found"),
|
|
29
|
+
buildCheck("resolved-path", "Resolved session source", resolvedPath ? "ok" : "warn", resolvedPath
|
|
30
|
+
? `${resolution.source}: ${basename(resolvedPath)}`
|
|
31
|
+
: "No session path could be resolved automatically"),
|
|
32
|
+
buildCheck("session-files", "Session files", sessionFiles.length > 0 ? "ok" : "warn", resolvedPath
|
|
33
|
+
? `${sessionFiles.length} .jsonl file(s) in ${resolvedPath}`
|
|
34
|
+
: "No resolved project to scan"),
|
|
35
|
+
buildCheck("hook", "SessionEnd hook", hookInstalled ? "ok" : "warn", hookInstalled
|
|
36
|
+
? `Installed in ${getSettingsPath()}`
|
|
37
|
+
: "Not installed. Run temet install-hook"),
|
|
38
|
+
buildCheck("narration", "Narration access", narrationAvailable ? "ok" : "warn", narrationAvailable
|
|
39
|
+
? "Token detected for LLM narration"
|
|
40
|
+
: "No ANTHROPIC_API_KEY or CLAUDE_AUTH_TOKEN detected"),
|
|
41
|
+
buildCheck("menubar-app", "Temet app", process.platform !== "darwin" ? "warn" : menubarInstalled ? "ok" : "warn", process.platform !== "darwin"
|
|
42
|
+
? "Temet app is only available on macOS"
|
|
43
|
+
: menubarInstalled
|
|
44
|
+
? `Installed at ${getMenubarAppPath()}`
|
|
45
|
+
: "Not installed. Run temet install-menubar"),
|
|
46
|
+
buildCheck("menubar-config", "Temet app config", process.platform !== "darwin"
|
|
47
|
+
? "warn"
|
|
48
|
+
: menubarConfigExists
|
|
49
|
+
? "ok"
|
|
50
|
+
: "warn", process.platform !== "darwin"
|
|
51
|
+
? "Not applicable on this platform"
|
|
52
|
+
: menubarConfigExists
|
|
53
|
+
? `Config present at ${getMenubarConfigPath()}`
|
|
54
|
+
: "No menubar config found yet"),
|
|
55
|
+
buildCheck("menubar-state", "Temet app state", menubarStateExists ? "ok" : "warn", menubarStateExists
|
|
56
|
+
? `Sidecar present at ${getMenubarStatePath()}`
|
|
57
|
+
: "No menubar state written yet. Run temet audit --track"),
|
|
58
|
+
];
|
|
59
|
+
const report = {
|
|
60
|
+
version: 1,
|
|
61
|
+
checks,
|
|
62
|
+
summary: {
|
|
63
|
+
ok: checks.filter((check) => check.status === "ok").length,
|
|
64
|
+
warn: checks.filter((check) => check.status === "warn").length,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
if (opts.json) {
|
|
68
|
+
console.log(JSON.stringify(report, null, "\t"));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
console.log("Temet doctor");
|
|
72
|
+
console.log("");
|
|
73
|
+
for (const check of checks) {
|
|
74
|
+
console.log(`${iconFor(check.status)} ${check.label}`);
|
|
75
|
+
console.log(` ${check.detail}`);
|
|
76
|
+
}
|
|
77
|
+
console.log("");
|
|
78
|
+
console.log(`Summary: ${report.summary.ok} OK, ${report.summary.warn} warning${report.summary.warn === 1 ? "" : "s"}`);
|
|
79
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
3
4
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
5
|
import { homedir } from "node:os";
|
|
5
6
|
import { dirname, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
6
8
|
import { promisify } from "node:util";
|
|
7
9
|
import { buildAuditCliOptions, parseFlagBag, readOptionalString, } from "./lib/cli-args.js";
|
|
8
10
|
const execFileAsync = promisify(execFile);
|
|
@@ -11,10 +13,22 @@ const DEFAULT_SERVER_NAME = "temet";
|
|
|
11
13
|
const DEFAULT_PROTOCOL_APP_NAME = "Temet Handler";
|
|
12
14
|
const LINUX_DESKTOP_ENTRY = "temet-handler.desktop";
|
|
13
15
|
const LINUX_HANDLER_SCRIPT = "temet-protocol-handler";
|
|
14
|
-
const
|
|
16
|
+
const PACKAGE_JSON_PATH = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
17
|
+
function readCliVersion() {
|
|
18
|
+
try {
|
|
19
|
+
const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, "utf8"));
|
|
20
|
+
return packageJson.version || "0.0.0";
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return "0.0.0";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const CLI_VERSION = readCliVersion();
|
|
27
|
+
const HELP = `Temet CLI ${CLI_VERSION} — discover the skills you already demonstrate in AI work
|
|
15
28
|
|
|
16
29
|
Commands:
|
|
17
30
|
\ttemet audit [--path <session-dir>] Analyze local sessions, surface skills and workflows
|
|
31
|
+
\ttemet doctor [--path <session-dir>] Check local Temet setup, sessions, hook, and app state
|
|
18
32
|
\ttemet traces [--path <session-dir>] Show the evidence behind the strongest signals
|
|
19
33
|
\ttemet plan [--path <session-dir>] Turn the current audit into an operating plan
|
|
20
34
|
\ttemet install-hook Auto-audit after every Claude Code session
|
|
@@ -41,6 +55,7 @@ Advanced:
|
|
|
41
55
|
|
|
42
56
|
Examples:
|
|
43
57
|
\ttemet audit Auto-detect sessions from cwd
|
|
58
|
+
\ttemet doctor Check whether Temet is fully wired on this machine
|
|
44
59
|
\ttemet audit Open the text report by default
|
|
45
60
|
\ttemet traces Inspect the proof behind the current reading
|
|
46
61
|
\ttemet plan Get the current operating plan
|
|
@@ -55,6 +70,10 @@ function printHelp(exitCode = 0) {
|
|
|
55
70
|
console.log(HELP);
|
|
56
71
|
process.exit(exitCode);
|
|
57
72
|
}
|
|
73
|
+
function printVersion() {
|
|
74
|
+
console.log(CLI_VERSION);
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
58
77
|
function isObject(value) {
|
|
59
78
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
79
|
}
|
|
@@ -147,6 +166,9 @@ function parseConnectUrlOptions(flags, positionals) {
|
|
|
147
166
|
};
|
|
148
167
|
}
|
|
149
168
|
function parseArgs(argv) {
|
|
169
|
+
if (argv.includes("-v") || argv.includes("--version")) {
|
|
170
|
+
printVersion();
|
|
171
|
+
}
|
|
150
172
|
if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
|
|
151
173
|
printHelp(0);
|
|
152
174
|
}
|
|
@@ -159,6 +181,15 @@ function parseArgs(argv) {
|
|
|
159
181
|
options: buildAuditCliOptions(flags, process.env),
|
|
160
182
|
};
|
|
161
183
|
}
|
|
184
|
+
if (command === "doctor") {
|
|
185
|
+
return {
|
|
186
|
+
command,
|
|
187
|
+
options: {
|
|
188
|
+
path: readOptionalString(flags, "path") ?? undefined,
|
|
189
|
+
json: Boolean(flags.get("json")),
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
162
193
|
if (command === "traces" || command === "plan") {
|
|
163
194
|
return {
|
|
164
195
|
command,
|
|
@@ -511,6 +542,11 @@ async function run() {
|
|
|
511
542
|
await runAuditCommand(parsed.options);
|
|
512
543
|
return;
|
|
513
544
|
}
|
|
545
|
+
if (parsed.command === "doctor") {
|
|
546
|
+
const { runDoctorCommand } = await import("./doctor.js");
|
|
547
|
+
await runDoctorCommand(parsed.options);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
514
550
|
if (parsed.command === "traces") {
|
|
515
551
|
const { runTracesCommand } = await import("./traces.js");
|
|
516
552
|
await runTracesCommand(parsed.options);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
|
-
import { resolveSessionPath } from "./path-resolver.js";
|
|
3
|
+
import { findCodexSessionFiles, findClaudeProjectCandidates, resolveSessionPath, } from "./path-resolver.js";
|
|
4
4
|
import { trackAuditSnapshot } from "./audit-tracking.js";
|
|
5
5
|
import { findSessionFiles, runAudit, } from "./session-audit.js";
|
|
6
6
|
async function chooseSessionCandidate(candidates) {
|
|
@@ -55,12 +55,40 @@ export async function resolveDiagnostics(opts) {
|
|
|
55
55
|
process.stderr.write(`[temet] using ${label}\n`);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
let sessionFiles = findSessionFiles(resolvedPath);
|
|
59
|
+
if (sessionFiles.length === 0 && !opts.path && !opts.quiet && !opts.json) {
|
|
60
|
+
const candidates = findClaudeProjectCandidates();
|
|
61
|
+
if (candidates.length > 0) {
|
|
62
|
+
const selected = await chooseSessionCandidate(candidates);
|
|
63
|
+
if (selected) {
|
|
64
|
+
resolvedPath = selected.sessionDir;
|
|
65
|
+
sessionFiles = findSessionFiles(resolvedPath);
|
|
66
|
+
process.stderr.write(`[temet] using ${selected.label}\n`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const codexFiles = !opts.path ? findCodexSessionFiles() : [];
|
|
71
|
+
// Include all Codex sessions (user-wide audit)
|
|
72
|
+
if (!opts.path) {
|
|
73
|
+
if (codexFiles.length > 0) {
|
|
74
|
+
sessionFiles = [...sessionFiles, ...codexFiles];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
59
77
|
if (sessionFiles.length === 0) {
|
|
60
|
-
throw new Error(
|
|
78
|
+
throw new Error(opts.path
|
|
79
|
+
? `No .jsonl session files found in ${resolvedPath}.`
|
|
80
|
+
: "No AI session files found on this machine.");
|
|
61
81
|
}
|
|
62
82
|
if (!opts.quiet) {
|
|
63
|
-
|
|
83
|
+
const codexCount = codexFiles.length;
|
|
84
|
+
const claudeCount = sessionFiles.length - codexCount;
|
|
85
|
+
const sources = [
|
|
86
|
+
claudeCount > 0 ? `${claudeCount} Claude` : "",
|
|
87
|
+
codexCount > 0 ? `${codexCount} Codex` : "",
|
|
88
|
+
]
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.join(" + ");
|
|
91
|
+
process.stderr.write(`[temet] reading ${sessionFiles.length} session file(s)${sources ? ` (${sources})` : ""}\n`);
|
|
64
92
|
}
|
|
65
93
|
const result = await runAudit(sessionFiles);
|
|
66
94
|
let competencies = result.competencies;
|
|
@@ -18,6 +18,14 @@ type ResolveSessionPathOptions = {
|
|
|
18
18
|
/** Convert an absolute project directory to the Claude Code session directory slug. */
|
|
19
19
|
export declare function cwdToSessionDir(cwd: string): string;
|
|
20
20
|
export declare function humanizeSessionDir(sessionDir: string): string;
|
|
21
|
+
export type DetectedSources = {
|
|
22
|
+
claude: boolean;
|
|
23
|
+
codex: boolean;
|
|
24
|
+
claudeCount: number;
|
|
25
|
+
codexCount: number;
|
|
26
|
+
};
|
|
27
|
+
export declare function detectAvailableSources(claudeRoot?: string, codexRoot?: string): DetectedSources;
|
|
28
|
+
export declare function findCodexSessionFiles(codexRoot?: string): string[];
|
|
21
29
|
export declare function findClaudeProjectCandidates(claudeRoot?: string): SessionCandidate[];
|
|
22
30
|
/**
|
|
23
31
|
* Try to read the JSON stdin from a Claude Code hook (non-blocking).
|
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
const DEFAULT_CLAUDE_ROOT = path.join(homedir(), ".claude", "projects");
|
|
5
|
+
const DEFAULT_CODEX_ROOT = path.join(homedir(), ".codex");
|
|
5
6
|
/** Convert an absolute project directory to the Claude Code session directory slug. */
|
|
6
7
|
export function cwdToSessionDir(cwd) {
|
|
7
8
|
const slug = path.resolve(cwd).replace(/\//g, "-");
|
|
@@ -51,6 +52,51 @@ function candidateFromSessionDir(sessionDir) {
|
|
|
51
52
|
updatedAtMs,
|
|
52
53
|
};
|
|
53
54
|
}
|
|
55
|
+
export function detectAvailableSources(claudeRoot = DEFAULT_CLAUDE_ROOT, codexRoot = DEFAULT_CODEX_ROOT) {
|
|
56
|
+
const claudeCandidates = findClaudeProjectCandidates(claudeRoot);
|
|
57
|
+
const codexFiles = findCodexSessionFiles(codexRoot);
|
|
58
|
+
return {
|
|
59
|
+
claude: claudeCandidates.length > 0,
|
|
60
|
+
codex: codexFiles.length > 0,
|
|
61
|
+
claudeCount: claudeCandidates.length,
|
|
62
|
+
codexCount: codexFiles.length,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function findCodexSessionFiles(codexRoot = DEFAULT_CODEX_ROOT) {
|
|
66
|
+
const files = [];
|
|
67
|
+
const dirs = [
|
|
68
|
+
path.join(codexRoot, "sessions"),
|
|
69
|
+
path.join(codexRoot, "archived_sessions"),
|
|
70
|
+
];
|
|
71
|
+
function walk(dir) {
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = readdirSync(dir);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const full = path.join(dir, entry);
|
|
81
|
+
try {
|
|
82
|
+
const st = statSync(full);
|
|
83
|
+
if (st.isDirectory()) {
|
|
84
|
+
walk(full);
|
|
85
|
+
}
|
|
86
|
+
else if (st.isFile() && entry.endsWith(".jsonl")) {
|
|
87
|
+
files.push(full);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// skip
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const dir of dirs) {
|
|
96
|
+
walk(dir);
|
|
97
|
+
}
|
|
98
|
+
return files.sort();
|
|
99
|
+
}
|
|
54
100
|
export function findClaudeProjectCandidates(claudeRoot = DEFAULT_CLAUDE_ROOT) {
|
|
55
101
|
try {
|
|
56
102
|
return readdirSync(claudeRoot)
|
|
@@ -80,7 +80,7 @@ export async function saveAuditTextReport(projectLabel, content, now = new Date(
|
|
|
80
80
|
}
|
|
81
81
|
export async function openReportFile(filePath, platform = process.platform) {
|
|
82
82
|
if (platform === "darwin") {
|
|
83
|
-
await execFileAsync("open", [
|
|
83
|
+
await execFileAsync("open", [filePath]);
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
if (platform === "win32") {
|
|
@@ -31,5 +31,7 @@ export type SessionStats = {
|
|
|
31
31
|
toolCalls: ToolCall[];
|
|
32
32
|
filesTouched: string[];
|
|
33
33
|
};
|
|
34
|
+
export type SessionFormat = "claude" | "codex";
|
|
35
|
+
export declare function detectSessionFormat(firstLine: string): SessionFormat;
|
|
34
36
|
export declare function parseSession(filePath: string): AsyncGenerator<SessionMessage>;
|
|
35
37
|
export declare function collectSession(filePath: string): Promise<SessionStats>;
|
|
@@ -65,8 +65,71 @@ function extractFilePathsFromToolInput(input) {
|
|
|
65
65
|
}
|
|
66
66
|
return paths;
|
|
67
67
|
}
|
|
68
|
-
// ----------
|
|
69
|
-
|
|
68
|
+
// ---------- Codex helpers ----------
|
|
69
|
+
/** Map Codex tool names to normalized equivalents for heuristic extraction. */
|
|
70
|
+
function mapCodexToolName(name) {
|
|
71
|
+
switch (name) {
|
|
72
|
+
case "exec_command":
|
|
73
|
+
case "shell":
|
|
74
|
+
case "shell_command":
|
|
75
|
+
return "Bash";
|
|
76
|
+
case "apply_patch":
|
|
77
|
+
return "Edit";
|
|
78
|
+
case "read_file":
|
|
79
|
+
return "Read";
|
|
80
|
+
case "write_stdin":
|
|
81
|
+
return "Bash";
|
|
82
|
+
case "update_plan":
|
|
83
|
+
return "TodoWrite";
|
|
84
|
+
case "view_image":
|
|
85
|
+
return "Read";
|
|
86
|
+
default:
|
|
87
|
+
return name;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function parseCodexArguments(raw) {
|
|
91
|
+
if (typeof raw === "string") {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(raw);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (raw && typeof raw === "object")
|
|
100
|
+
return raw;
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
function normalizeCodexInput(toolName, args) {
|
|
104
|
+
if (toolName === "exec_command" ||
|
|
105
|
+
toolName === "shell" ||
|
|
106
|
+
toolName === "shell_command") {
|
|
107
|
+
return { command: args.cmd ?? args.command ?? "" };
|
|
108
|
+
}
|
|
109
|
+
if (toolName === "apply_patch") {
|
|
110
|
+
return {
|
|
111
|
+
file_path: args.file_path ?? args.path ?? "",
|
|
112
|
+
old_string: "",
|
|
113
|
+
new_string: "",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return args;
|
|
117
|
+
}
|
|
118
|
+
export function detectSessionFormat(firstLine) {
|
|
119
|
+
try {
|
|
120
|
+
const entry = JSON.parse(firstLine);
|
|
121
|
+
if (entry.type === "session_meta")
|
|
122
|
+
return "codex";
|
|
123
|
+
if (entry.type === "response_item")
|
|
124
|
+
return "codex";
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// fall through
|
|
128
|
+
}
|
|
129
|
+
return "claude";
|
|
130
|
+
}
|
|
131
|
+
// ---------- Parsers ----------
|
|
132
|
+
async function* parseClaudeSession(filePath) {
|
|
70
133
|
const stream = createReadStream(filePath, { encoding: "utf-8" });
|
|
71
134
|
const rl = createInterface({
|
|
72
135
|
input: stream,
|
|
@@ -103,6 +166,82 @@ export async function* parseSession(filePath) {
|
|
|
103
166
|
};
|
|
104
167
|
}
|
|
105
168
|
}
|
|
169
|
+
async function* parseCodexSession(filePath) {
|
|
170
|
+
const stream = createReadStream(filePath, { encoding: "utf-8" });
|
|
171
|
+
const rl = createInterface({
|
|
172
|
+
input: stream,
|
|
173
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
174
|
+
});
|
|
175
|
+
for await (const line of rl) {
|
|
176
|
+
if (!line.trim())
|
|
177
|
+
continue;
|
|
178
|
+
let entry;
|
|
179
|
+
try {
|
|
180
|
+
entry = JSON.parse(line);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (entry.type !== "response_item")
|
|
186
|
+
continue;
|
|
187
|
+
const payload = entry.payload;
|
|
188
|
+
if (!payload)
|
|
189
|
+
continue;
|
|
190
|
+
const ts = entry.timestamp ?? "";
|
|
191
|
+
// User or assistant text messages
|
|
192
|
+
if (payload.type === "message") {
|
|
193
|
+
const role = payload.role;
|
|
194
|
+
if (role !== "user" && role !== "assistant")
|
|
195
|
+
continue;
|
|
196
|
+
const content = normalizeContent(payload.content);
|
|
197
|
+
if (content.length === 0)
|
|
198
|
+
continue;
|
|
199
|
+
yield { uuid: "", timestamp: ts, role, content };
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
// Function calls → normalize to tool_use
|
|
203
|
+
if (payload.type === "function_call" && typeof payload.name === "string") {
|
|
204
|
+
const args = parseCodexArguments(payload.arguments);
|
|
205
|
+
const mappedName = mapCodexToolName(payload.name);
|
|
206
|
+
const input = normalizeCodexInput(payload.name, args);
|
|
207
|
+
yield {
|
|
208
|
+
uuid: payload.call_id ?? "",
|
|
209
|
+
timestamp: ts,
|
|
210
|
+
role: "assistant",
|
|
211
|
+
content: [
|
|
212
|
+
{
|
|
213
|
+
type: "tool_use",
|
|
214
|
+
id: payload.call_id ?? "",
|
|
215
|
+
name: mappedName,
|
|
216
|
+
input,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export async function* parseSession(filePath) {
|
|
224
|
+
const stream = createReadStream(filePath, { encoding: "utf-8" });
|
|
225
|
+
const rl = createInterface({
|
|
226
|
+
input: stream,
|
|
227
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
228
|
+
});
|
|
229
|
+
let format = null;
|
|
230
|
+
for await (const line of rl) {
|
|
231
|
+
if (!line.trim())
|
|
232
|
+
continue;
|
|
233
|
+
format = detectSessionFormat(line);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
// Close the detection stream, re-open with the right parser
|
|
237
|
+
stream.destroy();
|
|
238
|
+
if (format === "codex") {
|
|
239
|
+
yield* parseCodexSession(filePath);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
yield* parseClaudeSession(filePath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
106
245
|
export async function collectSession(filePath) {
|
|
107
246
|
const messages = [];
|
|
108
247
|
const toolCalls = [];
|