@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,130 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ const SETTINGS_PATH = path.join(homedir(), ".claude", "settings.json");
6
+ const HOOK_MARKER = "audit --track --quiet --notify";
7
+ function shellQuote(s) {
8
+ if (/^[a-zA-Z0-9_/.:-]+$/.test(s))
9
+ return s;
10
+ return `'${s.replace(/'/g, "'\\''")}'`;
11
+ }
12
+ /** Resolve the absolute path for the temet binary at install time. */
13
+ export function resolveTemetBinary() {
14
+ // 1. The current entry point (most reliable — we're running it right now)
15
+ const selfEntry = process.argv[1];
16
+ if (selfEntry && existsSync(selfEntry)) {
17
+ return `${shellQuote(process.execPath)} ${shellQuote(selfEntry)}`;
18
+ }
19
+ // 2. which temet
20
+ try {
21
+ const result = execSync("which temet 2>/dev/null", {
22
+ encoding: "utf8",
23
+ }).trim();
24
+ if (result)
25
+ return shellQuote(result);
26
+ }
27
+ catch {
28
+ /* not found */
29
+ }
30
+ // 3. global npm install path
31
+ try {
32
+ const globalRoot = execSync("npm root -g 2>/dev/null", {
33
+ encoding: "utf8",
34
+ }).trim();
35
+ const candidate = path.join(globalRoot, "@temet/cli/dist/index.js");
36
+ if (existsSync(candidate)) {
37
+ return `${shellQuote(process.execPath)} ${shellQuote(candidate)}`;
38
+ }
39
+ }
40
+ catch {
41
+ /* not found */
42
+ }
43
+ return null;
44
+ }
45
+ export function readSettings(settingsPath) {
46
+ let raw;
47
+ try {
48
+ raw = readFileSync(settingsPath, "utf8");
49
+ }
50
+ catch (err) {
51
+ if (err.code === "ENOENT")
52
+ return {};
53
+ throw new Error(`Cannot read ${settingsPath}: ${err.message}`);
54
+ }
55
+ try {
56
+ const parsed = JSON.parse(raw);
57
+ if (typeof parsed === "object" &&
58
+ parsed !== null &&
59
+ !Array.isArray(parsed)) {
60
+ return parsed;
61
+ }
62
+ throw new Error(`${settingsPath} is not a JSON object`);
63
+ }
64
+ catch (err) {
65
+ if (err instanceof SyntaxError) {
66
+ throw new Error(`${settingsPath} contains invalid JSON — refusing to overwrite`);
67
+ }
68
+ throw err;
69
+ }
70
+ }
71
+ export function writeSettings(settingsPath, settings) {
72
+ mkdirSync(path.dirname(settingsPath), { recursive: true });
73
+ writeFileSync(settingsPath, JSON.stringify(settings, null, "\t") + "\n", "utf8");
74
+ }
75
+ function getSessionEndHooks(settings) {
76
+ const hooks = settings.hooks;
77
+ if (!hooks)
78
+ return null;
79
+ const sessionEnd = hooks.SessionEnd;
80
+ return Array.isArray(sessionEnd) ? sessionEnd : null;
81
+ }
82
+ /** Check if a temet SessionEnd hook is already installed. */
83
+ export function isHookInstalled(settings) {
84
+ const matchers = getSessionEndHooks(settings);
85
+ if (!matchers)
86
+ return false;
87
+ return matchers.some((m) => m.hooks?.some((h) => h.command?.includes(HOOK_MARKER)));
88
+ }
89
+ /** Add the SessionEnd hook entry. Idempotent. */
90
+ export function installHook(settings, binaryCommand) {
91
+ const command = `${binaryCommand} audit --track --quiet --notify >/dev/null 2>&1`;
92
+ const hookEntry = { type: "command", command, timeout: 60 };
93
+ const matcher = { matcher: "*", hooks: [hookEntry] };
94
+ if (!settings.hooks) {
95
+ settings.hooks = {};
96
+ }
97
+ const hooks = settings.hooks;
98
+ const existing = hooks.SessionEnd;
99
+ if (!Array.isArray(existing)) {
100
+ hooks.SessionEnd = [matcher];
101
+ return settings;
102
+ }
103
+ // Check if already present
104
+ const alreadyPresent = existing.some((m) => m.hooks?.some((h) => h.command?.includes(HOOK_MARKER)));
105
+ if (alreadyPresent)
106
+ return settings;
107
+ existing.push(matcher);
108
+ return settings;
109
+ }
110
+ /** Remove the temet SessionEnd hook entry. */
111
+ export function uninstallHook(settings) {
112
+ const hooks = settings.hooks;
113
+ if (!hooks)
114
+ return settings;
115
+ const existing = hooks.SessionEnd;
116
+ if (!Array.isArray(existing))
117
+ return settings;
118
+ hooks.SessionEnd = existing.filter((m) => !m.hooks?.some((h) => h.command?.includes(HOOK_MARKER)));
119
+ // Cleanup empty arrays and objects
120
+ if (hooks.SessionEnd.length === 0) {
121
+ delete hooks.SessionEnd;
122
+ }
123
+ if (Object.keys(hooks).length === 0) {
124
+ delete settings.hooks;
125
+ }
126
+ return settings;
127
+ }
128
+ export function getSettingsPath() {
129
+ return SETTINGS_PATH;
130
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Lightweight narrator — raw fetch to Anthropic Messages API.
3
+ * No AI SDK dependency.
4
+ *
5
+ * Auth priority:
6
+ * 1. CLAUDE_AUTH_TOKEN + CLAUDE_API_BASE_URL (Claude bearer proxy — primary)
7
+ * 2. ANTHROPIC_API_KEY + api.anthropic.com (direct Anthropic — explicit fallback)
8
+ */
9
+ import type { CompetencyEntry } from "./types.js";
10
+ import type { CombinedStats } from "./analysis-types.js";
11
+ import type { DetectedWorkflow } from "./workflow-detector.js";
12
+ export type NarrationResult = {
13
+ competencies: CompetencyEntry[];
14
+ bilan: string;
15
+ };
16
+ type AuthConfig = {
17
+ url: string;
18
+ headers: Record<string, string>;
19
+ source: string;
20
+ };
21
+ export declare function resolveAuth(): AuthConfig | null;
22
+ export declare function narrateCompetencies(raw: CompetencyEntry[], combined: CombinedStats, workflows: DetectedWorkflow[], options?: {
23
+ model?: string;
24
+ }): Promise<NarrationResult>;
25
+ export {};
@@ -0,0 +1,231 @@
1
+ // ---------- Excerpt Selection ----------
2
+ const CORRECTION_RX = [
3
+ /\bnon\b/i,
4
+ /\bpas comme [cç]a\b/i,
5
+ /\bc'est pas\b/i,
6
+ /\bplut[oô]t\b/i,
7
+ /\brevert\b/i,
8
+ /\bwrong\b/i,
9
+ /\bnot like that\b/i,
10
+ /\bundo\b/i,
11
+ /\bactually\b/i,
12
+ /\binstead\b/i,
13
+ ];
14
+ const DECISION_RX = [
15
+ /je pr[eé]f[eè]re/i,
16
+ /toujours\s/i,
17
+ /jamais\s/i,
18
+ /il faut/i,
19
+ /I prefer/i,
20
+ /always\s/i,
21
+ /never\s/i,
22
+ /we should/i,
23
+ /make sure to/i,
24
+ ];
25
+ function getTextContent(msg) {
26
+ return msg.content
27
+ .filter((b) => b.type === "text")
28
+ .map((b) => b.text)
29
+ .join("\n");
30
+ }
31
+ function scoreMessage(msg) {
32
+ const text = getTextContent(msg);
33
+ if (!text)
34
+ return 0;
35
+ let score = 0;
36
+ for (const rx of CORRECTION_RX) {
37
+ if (rx.test(text)) {
38
+ score += 3;
39
+ break;
40
+ }
41
+ }
42
+ for (const rx of DECISION_RX) {
43
+ if (rx.test(text)) {
44
+ score += 2;
45
+ break;
46
+ }
47
+ }
48
+ if (text.length > 500)
49
+ score += 1;
50
+ return score;
51
+ }
52
+ function selectExcerpts(combined, max = 10) {
53
+ return combined.messages
54
+ .filter((m) => m.role === "user")
55
+ .map((m) => ({ msg: m, score: scoreMessage(m) }))
56
+ .filter((s) => s.score > 0)
57
+ .sort((a, b) => b.score - a.score)
58
+ .slice(0, max)
59
+ .map((s) => ({
60
+ text: getTextContent(s.msg).slice(0, 300),
61
+ role: s.msg.role,
62
+ }));
63
+ }
64
+ // ---------- Prompt ----------
65
+ function buildPrompt(raw, excerpts, workflows) {
66
+ const competencyList = raw
67
+ .map((c) => `- ${c.name} [${c.proficiencyLevel}] (${c.category}): ${c.description}`)
68
+ .join("\n");
69
+ const excerptList = excerpts
70
+ .map((e, i) => `${i + 1}. "${e.text}"`)
71
+ .join("\n");
72
+ const workflowList = workflows.length > 0
73
+ ? workflows
74
+ .map((w) => `- ${w.description}: ${w.sequence.join(" → ")} (${w.occurrences}x across ${w.sessions} sessions)`)
75
+ .join("\n")
76
+ : "None detected.";
77
+ return `You are analyzing a developer's coding sessions to produce a professional competency profile.
78
+
79
+ ## Raw Competencies (from heuristic analysis)
80
+ ${competencyList}
81
+
82
+ ## User Excerpts (from sessions)
83
+ ${excerptList}
84
+
85
+ ## Detected Workflows
86
+ ${workflowList}
87
+
88
+ ## Instructions
89
+
90
+ Return a JSON object with exactly two keys:
91
+
92
+ 1. "competencies": an array of enriched competency objects. For each raw competency above, produce an object with these fields:
93
+ - "id": same as raw
94
+ - "name": same as raw
95
+ - "category": same as raw
96
+ - "proficiencyLevel": same as raw (adjust only if evidence strongly supports a different level)
97
+ - "description": a 1-2 sentence narrative description (not the raw heuristic output)
98
+ - "evidence": object with:
99
+ - "examples": array of 2-4 concrete examples drawn from the excerpts/workflows
100
+ - "decisionCriteria": array of 1-3 decision principles the developer follows
101
+ - "antiPatterns": array of 1-2 things the developer avoids
102
+ - "mentorAdvice": array of 1-2 insights about how this developer works
103
+ - "prerequisites": array of related skills needed (0-3)
104
+ - "synergies": array of complementary skills (0-3)
105
+
106
+ 2. "bilan": a markdown narrative summary (300-600 words) covering:
107
+ - Developer profile overview
108
+ - Key strengths and surprising patterns
109
+ - Workflow patterns and methodology
110
+ - Progression areas and recommendations
111
+
112
+ Keep descriptions professional but human. Ground everything in the evidence provided.
113
+ Return ONLY valid JSON, no markdown fences.`;
114
+ }
115
+ // ---------- Response Parser ----------
116
+ function ensureStringArray(value) {
117
+ if (!Array.isArray(value))
118
+ return [];
119
+ return value.filter((v) => typeof v === "string");
120
+ }
121
+ function parseResponse(text, fallback) {
122
+ try {
123
+ const cleaned = text
124
+ .replace(/^```json?\s*/m, "")
125
+ .replace(/```\s*$/m, "")
126
+ .trim();
127
+ const parsed = JSON.parse(cleaned);
128
+ if (!parsed.competencies ||
129
+ !Array.isArray(parsed.competencies) ||
130
+ !parsed.bilan ||
131
+ typeof parsed.bilan !== "string") {
132
+ return { competencies: fallback, bilan: "" };
133
+ }
134
+ const validated = [];
135
+ for (const c of parsed.competencies) {
136
+ if (!c ||
137
+ typeof c !== "object" ||
138
+ !c.id ||
139
+ !c.name ||
140
+ !c.category ||
141
+ !c.proficiencyLevel)
142
+ continue;
143
+ const ev = c.evidence && typeof c.evidence === "object" ? c.evidence : {};
144
+ validated.push({
145
+ ...c,
146
+ description: typeof c.description === "string" ? c.description : "",
147
+ evidence: {
148
+ examples: ensureStringArray(ev.examples),
149
+ decisionCriteria: ensureStringArray(ev.decisionCriteria),
150
+ antiPatterns: ensureStringArray(ev.antiPatterns),
151
+ mentorAdvice: ensureStringArray(ev.mentorAdvice),
152
+ },
153
+ prerequisites: ensureStringArray(c.prerequisites),
154
+ synergies: ensureStringArray(c.synergies),
155
+ });
156
+ }
157
+ if (validated.length === 0)
158
+ return { competencies: fallback, bilan: parsed.bilan ?? "" };
159
+ return { competencies: validated, bilan: parsed.bilan };
160
+ }
161
+ catch {
162
+ return { competencies: fallback, bilan: "" };
163
+ }
164
+ }
165
+ export function resolveAuth() {
166
+ const claudeToken = process.env.CLAUDE_AUTH_TOKEN;
167
+ const claudeBase = process.env.CLAUDE_API_BASE_URL;
168
+ // Primary: Claude bearer proxy (same path as the app's provider chain)
169
+ if (claudeToken) {
170
+ const base = claudeBase ?? "https://api.anthropic.com";
171
+ const url = base.includes("/v1/messages") ? base : `${base}/v1/messages`;
172
+ return {
173
+ url,
174
+ headers: {
175
+ "Content-Type": "application/json",
176
+ Authorization: `Bearer ${claudeToken}`,
177
+ "anthropic-version": "2023-06-01",
178
+ },
179
+ source: "CLAUDE_AUTH_TOKEN",
180
+ };
181
+ }
182
+ // Fallback: direct Anthropic API key
183
+ const apiKey = process.env.ANTHROPIC_API_KEY;
184
+ if (apiKey) {
185
+ return {
186
+ url: "https://api.anthropic.com/v1/messages",
187
+ headers: {
188
+ "Content-Type": "application/json",
189
+ "x-api-key": apiKey,
190
+ "anthropic-version": "2023-06-01",
191
+ },
192
+ source: "ANTHROPIC_API_KEY",
193
+ };
194
+ }
195
+ return null;
196
+ }
197
+ // ---------- API Call ----------
198
+ export async function narrateCompetencies(raw, combined, workflows, options) {
199
+ const auth = resolveAuth();
200
+ if (!auth) {
201
+ console.error("[temet] --narrate requires model access. Set ANTHROPIC_API_KEY or CLAUDE_AUTH_TOKEN.");
202
+ return { competencies: raw, bilan: "" };
203
+ }
204
+ const model = options?.model ?? "claude-haiku-4-5-20251001";
205
+ const excerpts = selectExcerpts(combined);
206
+ const prompt = buildPrompt(raw, excerpts, workflows);
207
+ console.error(`[temet] narrator using ${auth.source}`);
208
+ try {
209
+ const resp = await fetch(auth.url, {
210
+ method: "POST",
211
+ headers: auth.headers,
212
+ body: JSON.stringify({
213
+ model,
214
+ max_tokens: 4096,
215
+ messages: [{ role: "user", content: prompt }],
216
+ }),
217
+ });
218
+ if (!resp.ok) {
219
+ const body = await resp.text();
220
+ console.error(`[temet] narrator API error (${resp.status}): ${body}`);
221
+ return { competencies: raw, bilan: "" };
222
+ }
223
+ const data = (await resp.json());
224
+ const text = data.content?.find((b) => b.type === "text")?.text ?? "";
225
+ return parseResponse(text, raw);
226
+ }
227
+ catch (err) {
228
+ console.error("[temet] narrator call failed:", err);
229
+ return { competencies: raw, bilan: "" };
230
+ }
231
+ }
@@ -0,0 +1,9 @@
1
+ import type { AuditChange } from "./audit-tracking.js";
2
+ export type NotificationPayload = {
3
+ title: string;
4
+ body: string;
5
+ };
6
+ /** Build the notification text from audit changes. Returns null if nothing to notify. */
7
+ export declare function formatNotification(changes: AuditChange[], projectLabel: string): NotificationPayload | null;
8
+ /** Send a native OS notification. Fire-and-forget. */
9
+ export declare function sendNotification(payload: NotificationPayload): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import { execFile } from "node:child_process";
2
+ import { appendFile, mkdir } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ /** Build the notification text from audit changes. Returns null if nothing to notify. */
6
+ export function formatNotification(changes, projectLabel) {
7
+ if (changes.length === 0)
8
+ return null;
9
+ // Baseline
10
+ if (changes.length === 1 && changes[0].type === "baseline") {
11
+ const skillCount = changes[0].detail.match(/(\d+)/)?.[1] ?? "0";
12
+ return {
13
+ title: "Temet",
14
+ body: `Tracking started for ${projectLabel}`,
15
+ };
16
+ }
17
+ const meaningful = changes.filter((c) => c.type !== "baseline");
18
+ if (meaningful.length === 0)
19
+ return null;
20
+ const first = meaningful[0];
21
+ const body = meaningful.length === 1
22
+ ? first.title
23
+ : `${first.title} and ${meaningful.length - 1} more`;
24
+ return { title: "Temet", body };
25
+ }
26
+ /** Send a native OS notification. Fire-and-forget. */
27
+ export async function sendNotification(payload) {
28
+ const { title, body } = payload;
29
+ if (process.platform === "darwin") {
30
+ return new Promise((resolve, reject) => {
31
+ const escaped = body.replace(/"/g, '\\"');
32
+ const script = `display notification "${escaped}" with title "${title.replace(/"/g, '\\"')}"`;
33
+ execFile("osascript", ["-e", script], (err) => {
34
+ if (err)
35
+ reject(err);
36
+ else
37
+ resolve();
38
+ });
39
+ });
40
+ }
41
+ if (process.platform === "linux") {
42
+ return new Promise((resolve, reject) => {
43
+ execFile("notify-send", [title, body], (err) => {
44
+ if (err)
45
+ reject(err);
46
+ else
47
+ resolve();
48
+ });
49
+ });
50
+ }
51
+ // Fallback: log to file
52
+ const logDir = path.join(homedir(), ".temet");
53
+ await mkdir(logDir, { recursive: true });
54
+ const logPath = path.join(logDir, "notifications.log");
55
+ const line = `[${new Date().toISOString()}] ${title}: ${body}\n`;
56
+ await appendFile(logPath, line, "utf8");
57
+ }
@@ -0,0 +1,18 @@
1
+ /** Convert an absolute project directory to the Claude Code session directory slug. */
2
+ export declare function cwdToSessionDir(cwd: string): string;
3
+ /**
4
+ * Try to read the JSON stdin from a Claude Code hook (non-blocking).
5
+ * Only safe to call when we know stdin has finite piped data (hook context).
6
+ * Consumes stdin — call at most once per process.
7
+ */
8
+ export declare function readHookStdin(): {
9
+ cwd?: string;
10
+ } | null;
11
+ /**
12
+ * Resolve the session path by priority:
13
+ * 1. Explicit --path value
14
+ * 2. $CLAUDE_PROJECT_DIR env var
15
+ * 3. stdin JSON cwd (hook context)
16
+ * 4. process.cwd()
17
+ */
18
+ export declare function resolveSessionPath(explicit: string | undefined, env: NodeJS.ProcessEnv): string | null;
@@ -0,0 +1,56 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ /** Convert an absolute project directory to the Claude Code session directory slug. */
5
+ export function cwdToSessionDir(cwd) {
6
+ const slug = path.resolve(cwd).replace(/\//g, "-");
7
+ return path.join(homedir(), ".claude", "projects", slug);
8
+ }
9
+ /**
10
+ * Try to read the JSON stdin from a Claude Code hook (non-blocking).
11
+ * Only safe to call when we know stdin has finite piped data (hook context).
12
+ * Consumes stdin — call at most once per process.
13
+ */
14
+ export function readHookStdin() {
15
+ try {
16
+ // Only attempt if stdin is piped (non-TTY) AND we're in a hook context.
17
+ // CLAUDECODE env var is set when running inside Claude Code.
18
+ if (process.stdin.isTTY)
19
+ return null;
20
+ if (!process.env.CLAUDECODE)
21
+ return null;
22
+ const data = readFileSync(0, "utf8");
23
+ return JSON.parse(data);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ /**
30
+ * Resolve the session path by priority:
31
+ * 1. Explicit --path value
32
+ * 2. $CLAUDE_PROJECT_DIR env var
33
+ * 3. stdin JSON cwd (hook context)
34
+ * 4. process.cwd()
35
+ */
36
+ export function resolveSessionPath(explicit, env) {
37
+ if (explicit)
38
+ return explicit;
39
+ // Priority 2: CLAUDE_PROJECT_DIR env var (guaranteed in Claude Code hooks)
40
+ const projectDir = env.CLAUDE_PROJECT_DIR;
41
+ if (projectDir) {
42
+ const d = cwdToSessionDir(projectDir);
43
+ if (existsSync(d))
44
+ return d;
45
+ }
46
+ // Priority 3: stdin JSON cwd (hook context)
47
+ const hookInput = readHookStdin();
48
+ if (hookInput?.cwd) {
49
+ const d = cwdToSessionDir(hookInput.cwd);
50
+ if (existsSync(d))
51
+ return d;
52
+ }
53
+ // Priority 4: process.cwd()
54
+ const d = cwdToSessionDir(process.cwd());
55
+ return existsSync(d) ? d : null;
56
+ }
@@ -0,0 +1,23 @@
1
+ import type { CombinedStats } from "./analysis-types.js";
2
+ import { type Signal } from "./heuristics.js";
3
+ import { type DetectedWorkflow } from "./workflow-detector.js";
4
+ import type { CompetencyEntry } from "./types.js";
5
+ export type AuditResult = {
6
+ sessionCount: number;
7
+ messageCount: number;
8
+ toolCallCount: number;
9
+ signalCount: number;
10
+ workflowCount: number;
11
+ competencies: CompetencyEntry[];
12
+ workflows: DetectedWorkflow[];
13
+ signals: Signal[];
14
+ combined: CombinedStats;
15
+ };
16
+ export declare function findSessionFiles(dirPath: string): string[];
17
+ export declare function runAudit(sessionFiles: string[], onProgress?: (event: {
18
+ phase: "scan" | "signals" | "patterns";
19
+ file?: string;
20
+ current?: number;
21
+ total?: number;
22
+ elapsedMs?: number;
23
+ }) => void): Promise<AuditResult>;
@@ -0,0 +1,92 @@
1
+ // Adapted from scripts/lib/session-audit.ts — keep in sync
2
+ import { readdirSync, statSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { extractAllSignals } from "./heuristics.js";
5
+ import { collectSession } from "./session-parser.js";
6
+ import { mapSignalsToCompetencies } from "./skill-mapper.js";
7
+ import { detectWorkflows } from "./workflow-detector.js";
8
+ // ---------- Session Discovery ----------
9
+ export function findSessionFiles(dirPath) {
10
+ const resolved = path.resolve(dirPath);
11
+ const files = [];
12
+ function walk(dir) {
13
+ let entries;
14
+ try {
15
+ entries = readdirSync(dir);
16
+ }
17
+ catch {
18
+ return;
19
+ }
20
+ for (const entry of entries) {
21
+ const full = path.join(dir, entry);
22
+ try {
23
+ const st = statSync(full);
24
+ if (st.isDirectory()) {
25
+ walk(full);
26
+ }
27
+ else if (st.isFile() && entry.endsWith(".jsonl")) {
28
+ files.push(full);
29
+ }
30
+ }
31
+ catch {
32
+ // skip inaccessible entries
33
+ }
34
+ }
35
+ }
36
+ walk(resolved);
37
+ return files.sort();
38
+ }
39
+ // ---------- Pipeline ----------
40
+ export async function runAudit(sessionFiles, onProgress) {
41
+ const allSignals = [];
42
+ const combined = {
43
+ messages: [],
44
+ toolCalls: [],
45
+ filesTouched: [],
46
+ allToolCallsBySession: [],
47
+ };
48
+ const fileSet = new Set();
49
+ let signalMs = 0;
50
+ for (const file of sessionFiles) {
51
+ onProgress?.({
52
+ phase: "scan",
53
+ file: path.basename(file),
54
+ });
55
+ const stats = await collectSession(file);
56
+ combined.messages.push(...stats.messages);
57
+ combined.toolCalls.push(...stats.toolCalls);
58
+ combined.allToolCallsBySession.push(stats.toolCalls);
59
+ for (const fp of stats.filesTouched)
60
+ fileSet.add(fp);
61
+ const signalStartedAt = Date.now();
62
+ allSignals.push(...extractAllSignals(stats));
63
+ signalMs += Date.now() - signalStartedAt;
64
+ }
65
+ combined.filesTouched = [...fileSet];
66
+ onProgress?.({
67
+ phase: "signals",
68
+ current: allSignals.length,
69
+ total: allSignals.length,
70
+ elapsedMs: signalMs,
71
+ });
72
+ const workflowStartedAt = Date.now();
73
+ const workflows = detectWorkflows(combined.allToolCallsBySession);
74
+ onProgress?.({
75
+ phase: "patterns",
76
+ current: workflows.length,
77
+ total: workflows.length,
78
+ elapsedMs: Date.now() - workflowStartedAt,
79
+ });
80
+ const competencies = mapSignalsToCompetencies(allSignals);
81
+ return {
82
+ sessionCount: sessionFiles.length,
83
+ messageCount: combined.messages.length,
84
+ toolCallCount: combined.toolCalls.length,
85
+ signalCount: allSignals.length,
86
+ workflowCount: workflows.length,
87
+ competencies,
88
+ workflows,
89
+ signals: allSignals,
90
+ combined,
91
+ };
92
+ }
@@ -0,0 +1,35 @@
1
+ export type ContentBlock = {
2
+ type: "text";
3
+ text: string;
4
+ } | {
5
+ type: "tool_use";
6
+ id: string;
7
+ name: string;
8
+ input: unknown;
9
+ } | {
10
+ type: "thinking";
11
+ thinking: string;
12
+ } | {
13
+ type: "tool_result";
14
+ tool_use_id: string;
15
+ content: unknown;
16
+ };
17
+ export type SessionMessage = {
18
+ uuid: string;
19
+ timestamp: string;
20
+ role: "user" | "assistant";
21
+ content: ContentBlock[];
22
+ model?: string;
23
+ };
24
+ export type ToolCall = {
25
+ id: string;
26
+ name: string;
27
+ input: unknown;
28
+ };
29
+ export type SessionStats = {
30
+ messages: SessionMessage[];
31
+ toolCalls: ToolCall[];
32
+ filesTouched: string[];
33
+ };
34
+ export declare function parseSession(filePath: string): AsyncGenerator<SessionMessage>;
35
+ export declare function collectSession(filePath: string): Promise<SessionStats>;