Skip to content

Commit e66f900

Browse files
Validation skills and hooks integration.
1 parent 6ec6d3e commit e66f900

7 files changed

Lines changed: 2115 additions & 0 deletions

File tree

packages/altimate-code/script/build.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ const migrations = await Promise.all(
6666
)
6767
console.log(`Loaded ${migrations.length} migrations`)
6868

69+
// Load validate hook and skill assets for embedding
70+
const langfuseLogger = await Bun.file(path.join(dir, "src/hooks/langfuse_logger.py")).text()
71+
const validateSettings = await Bun.file(path.join(dir, "src/hooks/settings.json")).text()
72+
const validateSkillMd = await Bun.file(path.join(dir, "src/skills/validate/SKILL.md")).text()
73+
const validateBatchPy = await Bun.file(path.join(dir, "src/skills/validate/batch_validate.py")).text()
74+
console.log("Loaded validate hook and skill assets")
75+
6976
const singleFlag = process.argv.includes("--single")
7077
const baselineFlag = process.argv.includes("--baseline")
7178
const skipInstall = process.argv.includes("--skip-install")
@@ -213,6 +220,10 @@ for (const item of targets) {
213220
ALTIMATE_CLI_CHANNEL: `'${Script.channel}'`,
214221
ALTIMATE_ENGINE_VERSION: `'${engineVersion}'`,
215222
ALTIMATE_CLI_MIGRATIONS: JSON.stringify(migrations),
223+
ALTIMATE_VALIDATE_LANGFUSE_LOGGER: JSON.stringify(langfuseLogger),
224+
ALTIMATE_VALIDATE_SETTINGS: JSON.stringify(validateSettings),
225+
ALTIMATE_VALIDATE_SKILL_MD: JSON.stringify(validateSkillMd),
226+
ALTIMATE_VALIDATE_BATCH_PY: JSON.stringify(validateBatchPy),
216227
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
217228
},
218229
})
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import type { Argv } from "yargs"
2+
import { cmd } from "./cmd"
3+
import * as prompts from "@clack/prompts"
4+
import fs from "fs/promises"
5+
import path from "path"
6+
import os from "os"
7+
8+
// Injected at build time by build.ts (same pattern as ALTIMATE_CLI_MIGRATIONS).
9+
// In development these fall back to reading from disk via getAssets().
10+
declare const ALTIMATE_VALIDATE_LANGFUSE_LOGGER: string
11+
declare const ALTIMATE_VALIDATE_SETTINGS: string
12+
declare const ALTIMATE_VALIDATE_SKILL_MD: string
13+
declare const ALTIMATE_VALIDATE_BATCH_PY: string
14+
15+
interface ValidateAssets {
16+
loggerContent: string
17+
settingsContent: string
18+
skillMd: string
19+
batchPy: string
20+
}
21+
22+
async function getAssets(): Promise<ValidateAssets> {
23+
if (
24+
typeof ALTIMATE_VALIDATE_LANGFUSE_LOGGER !== "undefined" &&
25+
typeof ALTIMATE_VALIDATE_SETTINGS !== "undefined" &&
26+
typeof ALTIMATE_VALIDATE_SKILL_MD !== "undefined" &&
27+
typeof ALTIMATE_VALIDATE_BATCH_PY !== "undefined"
28+
) {
29+
return {
30+
loggerContent: ALTIMATE_VALIDATE_LANGFUSE_LOGGER,
31+
settingsContent: ALTIMATE_VALIDATE_SETTINGS,
32+
skillMd: ALTIMATE_VALIDATE_SKILL_MD,
33+
batchPy: ALTIMATE_VALIDATE_BATCH_PY,
34+
}
35+
}
36+
// Development fallback: read from disk relative to this source file
37+
const hooksDir = path.join(import.meta.dir, "../hooks")
38+
const skillsDir = path.join(import.meta.dir, "../skills/validate")
39+
const [loggerContent, settingsContent, skillMd, batchPy] = await Promise.all([
40+
fs.readFile(path.join(hooksDir, "langfuse_logger.py"), "utf-8"),
41+
fs.readFile(path.join(hooksDir, "settings.json"), "utf-8"),
42+
fs.readFile(path.join(skillsDir, "SKILL.md"), "utf-8"),
43+
fs.readFile(path.join(skillsDir, "batch_validate.py"), "utf-8"),
44+
])
45+
return { loggerContent, settingsContent, skillMd, batchPy }
46+
}
47+
48+
async function getClaudeDir(): Promise<string> {
49+
const home = os.homedir()
50+
const defaultDir = path.join(home, ".claude")
51+
52+
if (process.platform === "darwin" || process.platform === "win32") {
53+
return defaultDir
54+
}
55+
56+
// Linux: prefer ~/.claude if exists, else ~/.config/claude
57+
const exists = await fs
58+
.access(defaultDir)
59+
.then(() => true)
60+
.catch(() => false)
61+
return exists ? defaultDir : path.join(home, ".config", "claude")
62+
}
63+
64+
async function mergeHooks(sourceSettingsContent: string, targetSettingsPath: string): Promise<number> {
65+
const sourceData = JSON.parse(sourceSettingsContent)
66+
const sourceHooks: Record<string, any[]> = sourceData.hooks ?? {}
67+
68+
let targetData: Record<string, any> = {}
69+
const targetExists = await fs
70+
.access(targetSettingsPath)
71+
.then(() => true)
72+
.catch(() => false)
73+
if (targetExists) {
74+
const targetRaw = await fs.readFile(targetSettingsPath, "utf-8")
75+
targetData = JSON.parse(targetRaw)
76+
}
77+
78+
const targetHooks: Record<string, any[]> = (targetData.hooks ??= {})
79+
let mergedCount = 0
80+
81+
for (const [event, entries] of Object.entries(sourceHooks)) {
82+
if (!targetHooks[event]) {
83+
targetHooks[event] = entries
84+
mergedCount += entries.length
85+
} else {
86+
// Collect all existing command strings for this event
87+
const existingCommands = new Set<string>()
88+
for (const entry of targetHooks[event]) {
89+
for (const hook of entry.hooks ?? []) {
90+
if (hook.command) existingCommands.add(hook.command)
91+
}
92+
}
93+
94+
// Append source entries whose commands aren't already present
95+
for (const entry of entries) {
96+
const entryCommands = new Set((entry.hooks ?? []).map((h: any) => h.command).filter(Boolean))
97+
const hasOverlap = [...entryCommands].some((cmd) => existingCommands.has(cmd))
98+
if (!hasOverlap) {
99+
targetHooks[event].push(entry)
100+
mergedCount++
101+
}
102+
}
103+
}
104+
}
105+
106+
await fs.mkdir(path.dirname(targetSettingsPath), { recursive: true })
107+
await fs.writeFile(targetSettingsPath, JSON.stringify(targetData, null, 2))
108+
return mergedCount
109+
}
110+
111+
async function mergeEnvFile(envPath: string, vars: Record<string, string>): Promise<void> {
112+
const existing: Record<string, string> = {}
113+
114+
const fileExists = await fs
115+
.access(envPath)
116+
.then(() => true)
117+
.catch(() => false)
118+
if (fileExists) {
119+
const content = await fs.readFile(envPath, "utf-8")
120+
for (const line of content.split("\n")) {
121+
const trimmed = line.trim()
122+
if (!trimmed || trimmed.startsWith("#")) continue
123+
const eqIdx = trimmed.indexOf("=")
124+
if (eqIdx === -1) continue
125+
const key = trimmed.slice(0, eqIdx).trim()
126+
const value = trimmed.slice(eqIdx + 1).trim()
127+
existing[key] = value
128+
}
129+
}
130+
131+
Object.assign(existing, vars)
132+
await fs.writeFile(envPath, Object.entries(existing).map(([k, v]) => `${k}=${v}`).join("\n") + "\n")
133+
}
134+
135+
const InstallSubcommand = cmd({
136+
command: "install",
137+
describe: "install Langfuse logging hooks into ~/.claude",
138+
handler: async () => {
139+
prompts.intro("Altimate Validate — Hook Installer")
140+
141+
const claudeDir = await getClaudeDir()
142+
prompts.log.info(`Target directory: ${claudeDir}`)
143+
144+
const { loggerContent, settingsContent, skillMd, batchPy } = await getAssets()
145+
146+
// 1. Write langfuse_logger.py to ~/.claude/hooks/
147+
const spinner = prompts.spinner()
148+
spinner.start("Installing langfuse_logger.py...")
149+
const hooksTargetDir = path.join(claudeDir, "hooks")
150+
await fs.mkdir(hooksTargetDir, { recursive: true })
151+
const loggerTarget = path.join(hooksTargetDir, "langfuse_logger.py")
152+
await fs.writeFile(loggerTarget, loggerContent)
153+
spinner.stop(`Installed langfuse_logger.py → ${loggerTarget}`)
154+
155+
// 2. Merge hooks into ~/.claude/settings.json
156+
spinner.start("Merging hook config into settings.json...")
157+
const targetSettings = path.join(claudeDir, "settings.json")
158+
const added = await mergeHooks(settingsContent, targetSettings)
159+
spinner.stop(`settings.json updated (${added} new hook entries added)`)
160+
161+
// 3. Write skill files to ~/.altimate-code/skills/validate/
162+
spinner.start("Installing /validate skill...")
163+
const skillTargetDir = path.join(os.homedir(), ".altimate-code", "skills", "validate")
164+
await fs.mkdir(skillTargetDir, { recursive: true })
165+
await fs.writeFile(path.join(skillTargetDir, "SKILL.md"), skillMd)
166+
await fs.writeFile(path.join(skillTargetDir, "batch_validate.py"), batchPy)
167+
spinner.stop(`Installed /validate skill → ${skillTargetDir}`)
168+
169+
// 3. Prompt for Langfuse credentials
170+
prompts.log.message("")
171+
const configureLangfuse = await prompts.confirm({
172+
message: "Do you have a Langfuse account and want to configure your own credentials?",
173+
initialValue: false,
174+
})
175+
176+
if (!prompts.isCancel(configureLangfuse) && configureLangfuse) {
177+
const secretKey = await prompts.text({
178+
message: "LANGFUSE_SECRET_KEY_VALIDATION:",
179+
placeholder: "sk-lf-...",
180+
})
181+
const publicKey = await prompts.text({
182+
message: "LANGFUSE_PUBLIC_KEY_VALIDATION:",
183+
placeholder: "pk-lf-...",
184+
})
185+
const baseUrl = await prompts.text({
186+
message: "LANGFUSE_BASE_URL_VALIDATION:",
187+
placeholder: "https://cloud.langfuse.com",
188+
defaultValue: "https://cloud.langfuse.com",
189+
})
190+
191+
if (!prompts.isCancel(secretKey) && !prompts.isCancel(publicKey) && !prompts.isCancel(baseUrl)) {
192+
spinner.start("Writing Langfuse credentials to ~/.claude/.env...")
193+
const envPath = path.join(claudeDir, ".env")
194+
await mergeEnvFile(envPath, {
195+
LANGFUSE_SECRET_KEY_VALIDATION: secretKey as string,
196+
LANGFUSE_PUBLIC_KEY_VALIDATION: publicKey as string,
197+
LANGFUSE_BASE_URL_VALIDATION: (baseUrl as string) || "https://cloud.langfuse.com",
198+
})
199+
spinner.stop(`Credentials written to ${envPath}`)
200+
}
201+
} else {
202+
prompts.log.info("Skipping Langfuse configuration — default keys will be used.")
203+
}
204+
205+
prompts.outro("Altimate validation hooks installed successfully!")
206+
},
207+
})
208+
209+
const StatusSubcommand = cmd({
210+
command: "status",
211+
describe: "check whether Langfuse hooks are installed",
212+
handler: async () => {
213+
const claudeDir = await getClaudeDir()
214+
const loggerPath = path.join(claudeDir, "hooks", "langfuse_logger.py")
215+
const settingsPath = path.join(claudeDir, "settings.json")
216+
const envPath = path.join(claudeDir, ".env")
217+
const skillDir = path.join(os.homedir(), ".altimate-code", "skills", "validate")
218+
219+
prompts.intro("Altimate Validate — Installation Status")
220+
221+
const check = (exists: boolean, label: string, detail: string) =>
222+
prompts.log.info(`${exists ? "✓" : "✗"} ${label}${exists ? "" : " (not found)"}: ${detail}`)
223+
224+
// Hooks
225+
check(
226+
await fs.access(loggerPath).then(() => true).catch(() => false),
227+
"langfuse_logger.py",
228+
loggerPath,
229+
)
230+
231+
const settingsExists = await fs.access(settingsPath).then(() => true).catch(() => false)
232+
if (settingsExists) {
233+
const data = JSON.parse(await fs.readFile(settingsPath, "utf-8"))
234+
const hooks = data.hooks ?? {}
235+
const events = ["UserPromptSubmit", "PostToolUse", "Stop"]
236+
const configured = events.filter((e) => Array.isArray(hooks[e]) && hooks[e].length > 0)
237+
prompts.log.info(`${configured.length === 3 ? "✓" : "✗"} hook entries: ${configured.length}/3 configured in ${settingsPath}`)
238+
} else {
239+
prompts.log.info(`✗ settings.json: not found (${settingsPath})`)
240+
}
241+
242+
const envExists = await fs.access(envPath).then(() => true).catch(() => false)
243+
if (envExists) {
244+
const hasKey = (await fs.readFile(envPath, "utf-8")).includes("LANGFUSE_PUBLIC_KEY_VALIDATION")
245+
check(hasKey, "Langfuse credentials", envPath)
246+
} else {
247+
prompts.log.info(`✗ Langfuse credentials: .env not found (${envPath})`)
248+
}
249+
250+
// Skill
251+
const skillMdExists = await fs.access(path.join(skillDir, "SKILL.md")).then(() => true).catch(() => false)
252+
const batchPyExists = await fs.access(path.join(skillDir, "batch_validate.py")).then(() => true).catch(() => false)
253+
check(skillMdExists && batchPyExists, "/validate skill", skillDir)
254+
255+
prompts.outro("Done")
256+
},
257+
})
258+
259+
export const ValidateCommand = cmd({
260+
command: "validate",
261+
describe: "manage the Altimate validation framework (Langfuse hooks + /validate skill)",
262+
builder: (yargs: Argv) => yargs.command(InstallSubcommand).command(StatusSubcommand).demandCommand(),
263+
handler: () => {},
264+
})

0 commit comments

Comments
 (0)