@temet/cli 0.3.3 → 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 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 --path <dir> --track${RESET} Save a baseline and track changes over time`);
279
+ console.log(` ${DIM}temet audit --track${RESET} Keep learning from every Claude Code session`);
280
280
  }
281
- console.log(` ${DIM}temet traces${RESET} See the proof behind this reading`);
282
- console.log(` ${DIM}temet plan${RESET} Turn this audit into a focused next-step plan`);
283
- console.log(` ${DIM}temet audit --path <dir> --publish${RESET} Publish this audit to your Temet card`);
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
- rl.question(`\n${BOLD}${question}${RESET}\n${dim(hint, process.stderr)} `, (answer) => {
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;
@@ -472,17 +488,26 @@ export async function runAuditCommand(opts) {
472
488
  }
473
489
  }
474
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
+ }
475
496
  if (sessionFiles.length === 0) {
476
497
  if (!opts.quiet) {
477
- console.error(`[temet] no .jsonl session files found in ${resolvedPath}`);
498
+ console.error("[temet] no AI session files found on this machine");
478
499
  }
479
500
  process.exit(1);
480
501
  }
481
- if (!opts.json && !opts.quiet) {
482
- printBanner();
483
- }
484
502
  if (!opts.quiet) {
485
- console.error(`[temet] reading ${sessionFiles.length} saved session file(s)...`);
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})` : ""}`);
486
511
  }
487
512
  let completedFiles = 0;
488
513
  if (!opts.quiet) {
@@ -614,39 +639,57 @@ export async function runAuditCommand(opts) {
614
639
  const hookModule = await import("./lib/hook-installer.js");
615
640
  const settingsPath = hookModule.getSettingsPath();
616
641
  const settings = hookModule.readSettings(settingsPath);
617
- const hookAlreadyInstalled = hookModule.isHookInstalled(settings);
618
- if (!isMacOs()) {
619
- if (!opts.track && !hookAlreadyInstalled && !hasDeclinedHook()) {
620
- const answer = await askYesNo("Keep tracking your skills after each Claude Code session?", true);
621
- if (answer) {
622
- const binary = hookModule.resolveTemetBinary();
623
- if (binary) {
624
- const updated = hookModule.installHook(settings, binary);
625
- hookModule.writeSettings(settingsPath, updated);
626
- console.error(`${ok("Tracking enabled", process.stderr)} ${dim("You'll get a notification when your skills meaningfully change.", process.stderr)}`);
627
- }
628
- }
629
- else {
630
- saveHookDeclined();
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)}`);
631
663
  }
632
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
+ }
633
673
  }
634
674
  else {
635
- const { isMenubarInstalled, installMenubar } = await import("./lib/menubar-installer.js");
636
- if (!isMenubarInstalled()) {
637
- const answer = await askYesNo("Show Temet in your menu bar?", true, hookAlreadyInstalled
638
- ? "Press Enter to install Temet, or type no to skip:"
639
- : "Press Enter to install Temet and turn on automatic tracking, or type no to skip:");
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
+ });
640
680
  if (answer) {
641
681
  const result = await installMenubar({
642
682
  ensureHook: !hookAlreadyInstalled,
643
683
  });
644
684
  const suffix = result.hookInstalled
645
- ? "Temet opened and automatic tracking is now enabled."
646
- : "Temet opened.";
647
- console.error(`${ok("Temet installed", process.stderr)} ${dim(suffix, process.stderr)}`);
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)}`);
648
688
  }
649
689
  }
690
+ else if (menubarAlreadyInstalled) {
691
+ console.error(`${ok("✓", process.stderr)} ${dim("Temet is already in your menu bar.", process.stderr)}`);
692
+ }
650
693
  }
651
694
  }
652
695
  catch (error) {
package/dist/doctor.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { basename } from "node:path";
3
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";
4
+ import { getSettingsPath, isHookInstalled, readSettings, } from "./lib/hook-installer.js";
5
+ import { getMenubarAppPath, isMenubarInstalled, } from "./lib/menubar-installer.js";
6
6
  import { getMenubarConfigPath, getMenubarStatePath, } from "./lib/menubar-state.js";
7
7
  import { resolveAuth } from "./lib/narrator-lite.js";
8
8
  import { findSessionFiles } from "./lib/session-audit.js";
@@ -1,6 +1,6 @@
1
1
  import { basename } from "node:path";
2
2
  import { createInterface } from "node:readline";
3
- import { findClaudeProjectCandidates, 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) {
@@ -67,11 +67,28 @@ export async function resolveDiagnostics(opts) {
67
67
  }
68
68
  }
69
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
+ }
70
77
  if (sessionFiles.length === 0) {
71
- throw new Error(`No .jsonl session files found in ${resolvedPath}.`);
78
+ throw new Error(opts.path
79
+ ? `No .jsonl session files found in ${resolvedPath}.`
80
+ : "No AI session files found on this machine.");
72
81
  }
73
82
  if (!opts.quiet) {
74
- process.stderr.write(`[temet] reading ${sessionFiles.length} saved session file(s)...\n`);
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`);
75
92
  }
76
93
  const result = await runAudit(sessionFiles);
77
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", ["-a", "TextEdit", filePath]);
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
- // ---------- Parser ----------
69
- export async function* parseSession(filePath) {
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 = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@temet/cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Temet CLI — discover the skills you already demonstrate in AI work",
5
5
  "keywords": [
6
6
  "temet",