@temet/cli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.d.ts +34 -0
- package/dist/audit.js +531 -0
- package/dist/index.js +85 -40
- package/dist/lib/analysis-types.d.ts +7 -0
- package/dist/lib/analysis-types.js +1 -0
- package/dist/lib/audit-tracking.d.ts +55 -0
- package/dist/lib/audit-tracking.js +185 -0
- package/dist/lib/cli-args.d.ts +22 -0
- package/dist/lib/cli-args.js +65 -0
- package/dist/lib/editorial-taxonomy.d.ts +17 -0
- package/dist/lib/editorial-taxonomy.js +91 -0
- package/dist/lib/heuristics.d.ts +15 -0
- package/dist/lib/heuristics.js +341 -0
- package/dist/lib/hook-installer.d.ts +13 -0
- package/dist/lib/hook-installer.js +130 -0
- package/dist/lib/narrator-lite.d.ts +25 -0
- package/dist/lib/narrator-lite.js +231 -0
- package/dist/lib/notifier.d.ts +9 -0
- package/dist/lib/notifier.js +57 -0
- package/dist/lib/path-resolver.d.ts +39 -0
- package/dist/lib/path-resolver.js +152 -0
- package/dist/lib/profile-report.d.ts +18 -0
- package/dist/lib/profile-report.js +148 -0
- package/dist/lib/report-writer.d.ts +7 -0
- package/dist/lib/report-writer.js +73 -0
- package/dist/lib/session-audit.d.ts +24 -0
- package/dist/lib/session-audit.js +94 -0
- package/dist/lib/session-parser.d.ts +35 -0
- package/dist/lib/session-parser.js +130 -0
- package/dist/lib/skill-mapper.d.ts +3 -0
- package/dist/lib/skill-mapper.js +173 -0
- package/dist/lib/skill-naming.d.ts +1 -0
- package/dist/lib/skill-naming.js +50 -0
- package/dist/lib/types.d.ts +17 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/workflow-detector.d.ts +11 -0
- package/dist/lib/workflow-detector.js +125 -0
- package/package.json +2 -2
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// ---------- 1. Correction Patterns ----------
|
|
2
|
+
const CORRECTION_PATTERNS = [
|
|
3
|
+
// French
|
|
4
|
+
/\bnon\b/i,
|
|
5
|
+
/\bpas comme [cç]a\b/i,
|
|
6
|
+
/\bc'est pas\b/i,
|
|
7
|
+
/\bplut[oô]t\b/i,
|
|
8
|
+
/\brevert\b/i,
|
|
9
|
+
/\bannule\b/i,
|
|
10
|
+
/\brefais\b/i,
|
|
11
|
+
/\bmauvais\b/i,
|
|
12
|
+
/\bincorrect\b/i,
|
|
13
|
+
// English
|
|
14
|
+
/\bwrong\b/i,
|
|
15
|
+
/\bnot like that\b/i,
|
|
16
|
+
/\bno[,.]?\s/i,
|
|
17
|
+
/\bundo\b/i,
|
|
18
|
+
/\bdon't\b/i,
|
|
19
|
+
/\bshouldn't\b/i,
|
|
20
|
+
/\bactually\b/i,
|
|
21
|
+
/\binstead\b/i,
|
|
22
|
+
];
|
|
23
|
+
export function extractCorrectionPatterns(stats) {
|
|
24
|
+
const signals = [];
|
|
25
|
+
const { messages } = stats;
|
|
26
|
+
for (let i = 1; i < messages.length; i++) {
|
|
27
|
+
const msg = messages[i];
|
|
28
|
+
if (msg.role !== "user")
|
|
29
|
+
continue;
|
|
30
|
+
// Check if previous message was from assistant
|
|
31
|
+
const prev = messages[i - 1];
|
|
32
|
+
if (!prev || prev.role !== "assistant")
|
|
33
|
+
continue;
|
|
34
|
+
const userText = getTextContent(msg);
|
|
35
|
+
if (!userText)
|
|
36
|
+
continue;
|
|
37
|
+
for (const pattern of CORRECTION_PATTERNS) {
|
|
38
|
+
if (pattern.test(userText)) {
|
|
39
|
+
// Extract what the assistant was doing from the previous message
|
|
40
|
+
const assistantContext = getTextContent(prev);
|
|
41
|
+
const toolNames = getToolNames(prev);
|
|
42
|
+
const context = toolNames.length > 0
|
|
43
|
+
? `tools: ${toolNames.join(", ")}`
|
|
44
|
+
: truncate(assistantContext, 100);
|
|
45
|
+
signals.push({
|
|
46
|
+
type: "correction",
|
|
47
|
+
skill: inferSkillFromCorrection(userText, context),
|
|
48
|
+
confidence: 0.6,
|
|
49
|
+
evidence: `User correction: "${truncate(userText, 120)}" (after ${context})`,
|
|
50
|
+
category: "judgment",
|
|
51
|
+
});
|
|
52
|
+
break; // One signal per message
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return signals;
|
|
57
|
+
}
|
|
58
|
+
function inferSkillFromCorrection(userText, context) {
|
|
59
|
+
const text = `${userText} ${context}`.toLowerCase();
|
|
60
|
+
if (/git|commit|branch|push|merge/.test(text))
|
|
61
|
+
return "version control judgment";
|
|
62
|
+
if (/test|spec|assert/.test(text))
|
|
63
|
+
return "testing methodology";
|
|
64
|
+
if (/css|style|layout|design/.test(text))
|
|
65
|
+
return "UI/UX design sense";
|
|
66
|
+
if (/type|interface|schema/.test(text))
|
|
67
|
+
return "type system design";
|
|
68
|
+
if (/security|auth|token/.test(text))
|
|
69
|
+
return "security awareness";
|
|
70
|
+
if (/perf|optim|speed|slow/.test(text))
|
|
71
|
+
return "performance optimization";
|
|
72
|
+
if (/api|route|endpoint/.test(text))
|
|
73
|
+
return "API design";
|
|
74
|
+
if (/archi|struct|pattern/.test(text))
|
|
75
|
+
return "software architecture";
|
|
76
|
+
return "code review judgment";
|
|
77
|
+
}
|
|
78
|
+
// ---------- 2. Tool Frequency ----------
|
|
79
|
+
const TOOL_CLUSTERS = {
|
|
80
|
+
Grep: { skill: "code archaeology", category: "tool_proficiency" },
|
|
81
|
+
Glob: { skill: "codebase navigation", category: "tool_proficiency" },
|
|
82
|
+
Read: { skill: "code comprehension", category: "hard_skill" },
|
|
83
|
+
Edit: { skill: "code editing precision", category: "hard_skill" },
|
|
84
|
+
Write: { skill: "code generation", category: "hard_skill" },
|
|
85
|
+
Bash: { skill: "shell scripting", category: "tool_proficiency" },
|
|
86
|
+
Agent: { skill: "task delegation", category: "methodology" },
|
|
87
|
+
WebSearch: { skill: "research methodology", category: "methodology" },
|
|
88
|
+
WebFetch: { skill: "API integration", category: "hard_skill" },
|
|
89
|
+
};
|
|
90
|
+
export function extractToolFrequency(stats) {
|
|
91
|
+
const signals = [];
|
|
92
|
+
const counts = new Map();
|
|
93
|
+
for (const tc of stats.toolCalls) {
|
|
94
|
+
counts.set(tc.name, (counts.get(tc.name) ?? 0) + 1);
|
|
95
|
+
}
|
|
96
|
+
const total = stats.toolCalls.length;
|
|
97
|
+
if (total === 0)
|
|
98
|
+
return signals;
|
|
99
|
+
// Also detect git usage in Bash commands
|
|
100
|
+
let gitCount = 0;
|
|
101
|
+
for (const tc of stats.toolCalls) {
|
|
102
|
+
if (tc.name === "Bash") {
|
|
103
|
+
const cmd = tc.input?.command;
|
|
104
|
+
if (typeof cmd === "string" && /\bgit\b/.test(cmd)) {
|
|
105
|
+
gitCount++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (gitCount > 3) {
|
|
110
|
+
const freq = gitCount / total;
|
|
111
|
+
signals.push({
|
|
112
|
+
type: "tool_frequency",
|
|
113
|
+
skill: "version control mastery",
|
|
114
|
+
confidence: Math.min(0.5 + freq * 2, 0.95),
|
|
115
|
+
evidence: `${gitCount} git commands out of ${total} total tool calls (${(freq * 100).toFixed(1)}%)`,
|
|
116
|
+
category: "tool_proficiency",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
for (const [tool, info] of Object.entries(TOOL_CLUSTERS)) {
|
|
120
|
+
const count = counts.get(tool) ?? 0;
|
|
121
|
+
if (count < 3)
|
|
122
|
+
continue;
|
|
123
|
+
const freq = count / total;
|
|
124
|
+
signals.push({
|
|
125
|
+
type: "tool_frequency",
|
|
126
|
+
skill: info.skill,
|
|
127
|
+
confidence: Math.min(0.4 + freq * 2, 0.9),
|
|
128
|
+
evidence: `${tool} used ${count}/${total} times (${(freq * 100).toFixed(1)}%)`,
|
|
129
|
+
category: info.category,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return signals;
|
|
133
|
+
}
|
|
134
|
+
// ---------- 3. Decision Language ----------
|
|
135
|
+
const DECISION_PATTERNS = [
|
|
136
|
+
// French
|
|
137
|
+
{ pattern: /je pr[eé]f[eè]re\s+(.{10,80})/i, mapTo: "preference" },
|
|
138
|
+
{ pattern: /on va plut[oô]t\s+(.{10,80})/i, mapTo: "preference" },
|
|
139
|
+
{ pattern: /toujours\s+(.{10,80})/i, mapTo: "always" },
|
|
140
|
+
{ pattern: /jamais\s+(.{10,80})/i, mapTo: "never" },
|
|
141
|
+
{ pattern: /il faut\s+(.{10,80})/i, mapTo: "always" },
|
|
142
|
+
{ pattern: /on ne fait pas\s+(.{10,80})/i, mapTo: "never" },
|
|
143
|
+
// English
|
|
144
|
+
{ pattern: /I prefer\s+(.{10,80})/i, mapTo: "preference" },
|
|
145
|
+
{ pattern: /always\s+(.{10,80})/i, mapTo: "always" },
|
|
146
|
+
{ pattern: /never\s+(.{10,80})/i, mapTo: "never" },
|
|
147
|
+
{ pattern: /we should\s+(.{10,80})/i, mapTo: "preference" },
|
|
148
|
+
{ pattern: /don'?t ever\s+(.{10,80})/i, mapTo: "never" },
|
|
149
|
+
{ pattern: /make sure to\s+(.{10,80})/i, mapTo: "always" },
|
|
150
|
+
];
|
|
151
|
+
export function extractDecisionLanguage(stats) {
|
|
152
|
+
const signals = [];
|
|
153
|
+
for (const msg of stats.messages) {
|
|
154
|
+
if (msg.role !== "user")
|
|
155
|
+
continue;
|
|
156
|
+
const text = getTextContent(msg);
|
|
157
|
+
if (!text)
|
|
158
|
+
continue;
|
|
159
|
+
for (const { pattern, mapTo } of DECISION_PATTERNS) {
|
|
160
|
+
const match = pattern.exec(text);
|
|
161
|
+
if (!match)
|
|
162
|
+
continue;
|
|
163
|
+
const criterion = match[1].trim().replace(/[.!,;]+$/, "");
|
|
164
|
+
const skill = inferSkillFromDecision(criterion);
|
|
165
|
+
signals.push({
|
|
166
|
+
type: "decision_language",
|
|
167
|
+
skill,
|
|
168
|
+
confidence: mapTo === "always" || mapTo === "never" ? 0.7 : 0.5,
|
|
169
|
+
evidence: `${mapTo === "never" ? "Anti-pattern" : "Decision criterion"}: "${truncate(match[0], 100)}"`,
|
|
170
|
+
category: "judgment",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return signals;
|
|
175
|
+
}
|
|
176
|
+
function inferSkillFromDecision(text) {
|
|
177
|
+
const lower = text.toLowerCase();
|
|
178
|
+
if (/tab|indent|format|lint|biome/.test(lower))
|
|
179
|
+
return "code style standards";
|
|
180
|
+
if (/test|coverage|spec/.test(lower))
|
|
181
|
+
return "testing methodology";
|
|
182
|
+
if (/secur|auth|token|secret|\.env|credential/.test(lower))
|
|
183
|
+
return "security practices";
|
|
184
|
+
if (/commit|branch|pr|merge/.test(lower))
|
|
185
|
+
return "version control workflow";
|
|
186
|
+
if (/type|interface|generic/.test(lower))
|
|
187
|
+
return "type system design";
|
|
188
|
+
if (/error|catch|throw|handle/.test(lower))
|
|
189
|
+
return "error handling strategy";
|
|
190
|
+
if (/component|render|ui/.test(lower))
|
|
191
|
+
return "component architecture";
|
|
192
|
+
if (/api|route|endpoint|rest/.test(lower))
|
|
193
|
+
return "API design";
|
|
194
|
+
if (/perf|cache|optim/.test(lower))
|
|
195
|
+
return "performance optimization";
|
|
196
|
+
return "engineering judgment";
|
|
197
|
+
}
|
|
198
|
+
// ---------- 4. Domain Clustering ----------
|
|
199
|
+
const DOMAIN_MAP = {
|
|
200
|
+
"lib/a2a": { skill: "agent-to-agent protocol", category: "domain_knowledge" },
|
|
201
|
+
"lib/ai": { skill: "AI/LLM integration", category: "domain_knowledge" },
|
|
202
|
+
"lib/competency": {
|
|
203
|
+
skill: "competency modeling",
|
|
204
|
+
category: "domain_knowledge",
|
|
205
|
+
},
|
|
206
|
+
"lib/db": { skill: "database design", category: "hard_skill" },
|
|
207
|
+
"lib/generative-ui": {
|
|
208
|
+
skill: "generative UI systems",
|
|
209
|
+
category: "hard_skill",
|
|
210
|
+
},
|
|
211
|
+
"components/": {
|
|
212
|
+
skill: "React component development",
|
|
213
|
+
category: "hard_skill",
|
|
214
|
+
},
|
|
215
|
+
"relay-worker": {
|
|
216
|
+
skill: "Cloudflare Workers development",
|
|
217
|
+
category: "hard_skill",
|
|
218
|
+
},
|
|
219
|
+
"app/api": { skill: "Next.js API routes", category: "hard_skill" },
|
|
220
|
+
"app/(chat)": {
|
|
221
|
+
skill: "chat application architecture",
|
|
222
|
+
category: "domain_knowledge",
|
|
223
|
+
},
|
|
224
|
+
scripts: {
|
|
225
|
+
skill: "build tooling and automation",
|
|
226
|
+
category: "tool_proficiency",
|
|
227
|
+
},
|
|
228
|
+
".github": { skill: "CI/CD pipeline design", category: "methodology" },
|
|
229
|
+
content: { skill: "content strategy", category: "domain_knowledge" },
|
|
230
|
+
};
|
|
231
|
+
export function extractDomainClusters(stats) {
|
|
232
|
+
const signals = [];
|
|
233
|
+
const domainCounts = new Map();
|
|
234
|
+
for (const fp of stats.filesTouched) {
|
|
235
|
+
for (const [prefix, _info] of Object.entries(DOMAIN_MAP)) {
|
|
236
|
+
if (fp.includes(prefix)) {
|
|
237
|
+
domainCounts.set(prefix, (domainCounts.get(prefix) ?? 0) + 1);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const totalFiles = stats.filesTouched.length;
|
|
243
|
+
if (totalFiles === 0)
|
|
244
|
+
return signals;
|
|
245
|
+
for (const [prefix, count] of domainCounts) {
|
|
246
|
+
if (count < 2)
|
|
247
|
+
continue;
|
|
248
|
+
const info = DOMAIN_MAP[prefix];
|
|
249
|
+
const freq = count / totalFiles;
|
|
250
|
+
signals.push({
|
|
251
|
+
type: "domain_cluster",
|
|
252
|
+
skill: info.skill,
|
|
253
|
+
confidence: Math.min(0.4 + freq * 1.5, 0.85),
|
|
254
|
+
evidence: `${count} files touched in ${prefix} (${(freq * 100).toFixed(1)}% of ${totalFiles} files)`,
|
|
255
|
+
category: info.category,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return signals;
|
|
259
|
+
}
|
|
260
|
+
// ---------- 5. Prompt Structure ----------
|
|
261
|
+
export function extractPromptStructure(stats) {
|
|
262
|
+
const signals = [];
|
|
263
|
+
let longStructuredCount = 0;
|
|
264
|
+
let shortImperativeCount = 0;
|
|
265
|
+
let totalUserMessages = 0;
|
|
266
|
+
for (const msg of stats.messages) {
|
|
267
|
+
if (msg.role !== "user")
|
|
268
|
+
continue;
|
|
269
|
+
const text = getTextContent(msg);
|
|
270
|
+
if (!text)
|
|
271
|
+
continue;
|
|
272
|
+
// Skip tool results (user messages that are just tool output)
|
|
273
|
+
if (msg.content.some((b) => b.type === "tool_result"))
|
|
274
|
+
continue;
|
|
275
|
+
totalUserMessages++;
|
|
276
|
+
const len = text.length;
|
|
277
|
+
// Long numbered prompts = spec methodology
|
|
278
|
+
if (len > 500 && /(?:\d+[.)]\s|\n[-*]\s)/.test(text)) {
|
|
279
|
+
longStructuredCount++;
|
|
280
|
+
}
|
|
281
|
+
// Short imperatives = rapid iteration
|
|
282
|
+
else if (len < 100) {
|
|
283
|
+
shortImperativeCount++;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (totalUserMessages === 0)
|
|
287
|
+
return signals;
|
|
288
|
+
if (longStructuredCount >= 2) {
|
|
289
|
+
const freq = longStructuredCount / totalUserMessages;
|
|
290
|
+
signals.push({
|
|
291
|
+
type: "prompt_structure",
|
|
292
|
+
skill: "specification methodology",
|
|
293
|
+
confidence: Math.min(0.5 + freq * 2, 0.9),
|
|
294
|
+
evidence: `${longStructuredCount}/${totalUserMessages} user messages are long structured specs (>500 chars with numbered lists)`,
|
|
295
|
+
category: "methodology",
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (shortImperativeCount >= 5) {
|
|
299
|
+
const freq = shortImperativeCount / totalUserMessages;
|
|
300
|
+
signals.push({
|
|
301
|
+
type: "prompt_structure",
|
|
302
|
+
skill: "rapid iteration workflow",
|
|
303
|
+
confidence: Math.min(0.4 + freq, 0.85),
|
|
304
|
+
evidence: `${shortImperativeCount}/${totalUserMessages} user messages are short imperatives (<100 chars)`,
|
|
305
|
+
category: "methodology",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return signals;
|
|
309
|
+
}
|
|
310
|
+
// ---------- All extractors ----------
|
|
311
|
+
export const ALL_EXTRACTORS = [
|
|
312
|
+
extractCorrectionPatterns,
|
|
313
|
+
extractToolFrequency,
|
|
314
|
+
extractDecisionLanguage,
|
|
315
|
+
extractDomainClusters,
|
|
316
|
+
extractPromptStructure,
|
|
317
|
+
];
|
|
318
|
+
export function extractAllSignals(stats) {
|
|
319
|
+
const signals = [];
|
|
320
|
+
for (const extractor of ALL_EXTRACTORS) {
|
|
321
|
+
signals.push(...extractor(stats));
|
|
322
|
+
}
|
|
323
|
+
return signals;
|
|
324
|
+
}
|
|
325
|
+
// ---------- Util ----------
|
|
326
|
+
function getTextContent(msg) {
|
|
327
|
+
return msg.content
|
|
328
|
+
.filter((b) => b.type === "text")
|
|
329
|
+
.map((b) => b.text)
|
|
330
|
+
.join("\n");
|
|
331
|
+
}
|
|
332
|
+
function getToolNames(msg) {
|
|
333
|
+
return msg.content
|
|
334
|
+
.filter((b) => b.type === "tool_use")
|
|
335
|
+
.map((b) => b.name);
|
|
336
|
+
}
|
|
337
|
+
function truncate(s, max) {
|
|
338
|
+
if (s.length <= max)
|
|
339
|
+
return s;
|
|
340
|
+
return `${s.slice(0, max)}...`;
|
|
341
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type JsonObject = Record<string, unknown>;
|
|
2
|
+
/** Resolve the absolute path for the temet binary at install time. */
|
|
3
|
+
export declare function resolveTemetBinary(): string | null;
|
|
4
|
+
export declare function readSettings(settingsPath: string): JsonObject;
|
|
5
|
+
export declare function writeSettings(settingsPath: string, settings: JsonObject): void;
|
|
6
|
+
/** Check if a temet SessionEnd hook is already installed. */
|
|
7
|
+
export declare function isHookInstalled(settings: JsonObject): boolean;
|
|
8
|
+
/** Add the SessionEnd hook entry. Idempotent. */
|
|
9
|
+
export declare function installHook(settings: JsonObject, binaryCommand: string): JsonObject;
|
|
10
|
+
/** Remove the temet SessionEnd hook entry. */
|
|
11
|
+
export declare function uninstallHook(settings: JsonObject): JsonObject;
|
|
12
|
+
export declare function getSettingsPath(): string;
|
|
13
|
+
export {};
|
|
@@ -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 {};
|