Skip to content

Commit 3789318

Browse files
feat: auto-install validate skill and logger hook on startup
On every `InstanceBootstrap` (TUI session start), automatically: - Copy `SKILL.md` + `batch_validate.py` → `~/.claude/skills/validate/` - Write `altimate_logger_hook.py` → `~/.claude/hooks/` and merge Stop hook into `~/.claude/settings.json` (skipped if `ALTIMATE_LOGGER_DISABLED=true`) The logger hook reads the session JSONL transcript after each assistant response and posts the conversation turn to `/log-conversation` in the same payload format as `conversation-logger.ts`. Removed `validate install` from `bun run build` — no manual setup needed. Uses `globalThis` lookup for build-time globals so dev mode (`bun run dev`) falls back to reading files directly from disk. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2a2227c commit 3789318

File tree

4 files changed

+379
-2
lines changed

4 files changed

+379
-2
lines changed

packages/opencode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"scripts": {
99
"typecheck": "tsgo --noEmit",
1010
"test": "bun test --timeout 30000",
11-
"build": "bun run script/build.ts && bun run --conditions=browser ./src/index.ts validate install",
11+
"build": "bun run script/build.ts",
1212
"dev": "bun run --conditions=browser ./src/index.ts",
1313
"db": "bun drizzle-kit"
1414
},

packages/opencode/script/build.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ const validateSkillMd = await Bun.file(path.join(dir, "src/skill/validate/SKILL.
7676
const validateBatchPy = await Bun.file(path.join(dir, "src/skill/validate/batch_validate.py")).text()
7777
console.log("Loaded validate skill assets")
7878

79+
// Load logger hook for embedding
80+
const loggerHookPy = await Bun.file(path.join(dir, "src/skill/validate/logger_hook.py")).text()
81+
console.log("Loaded logger hook")
82+
7983
const singleFlag = process.argv.includes("--single")
8084
const baselineFlag = process.argv.includes("--baseline")
8185
const skipInstall = process.argv.includes("--skip-install")
@@ -227,6 +231,7 @@ for (const item of targets) {
227231
OPENCODE_CHANGELOG: JSON.stringify(changelog),
228232
ALTIMATE_VALIDATE_SKILL_MD: JSON.stringify(validateSkillMd),
229233
ALTIMATE_VALIDATE_BATCH_PY: JSON.stringify(validateBatchPy),
234+
ALTIMATE_LOGGER_HOOK_PY: JSON.stringify(loggerHookPy),
230235
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
231236
},
232237
})

packages/opencode/src/project/bootstrap.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,89 @@ import { ShareNext } from "@/share/share-next"
1313
import { Snapshot } from "../snapshot"
1414
import { Truncate } from "../tool/truncation"
1515
import { initConversationLogger } from "../session/conversation-logger"
16+
import fs from "fs/promises"
17+
import path from "path"
18+
import os from "os"
19+
20+
21+
function getClaudeDir(): string {
22+
if (process.platform === "win32") {
23+
return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude")
24+
}
25+
return path.join(os.homedir(), ".claude")
26+
}
27+
28+
async function readAsset(globalName: string, fallbackRelPath: string): Promise<string> {
29+
const val = (globalThis as any)[globalName]
30+
if (typeof val === "string" && val) return val
31+
return fs.readFile(path.join(import.meta.dir, fallbackRelPath), "utf-8")
32+
}
33+
34+
async function mergeStopHook(settingsPath: string, hookCommand: string): Promise<void> {
35+
let settings: Record<string, any> = {}
36+
try {
37+
settings = JSON.parse(await fs.readFile(settingsPath, "utf-8"))
38+
} catch {
39+
// Missing or unparseable — start fresh
40+
}
41+
42+
if (!settings.hooks) settings.hooks = {}
43+
if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = []
44+
45+
const alreadyExists = settings.hooks.Stop.some(
46+
(entry: any) =>
47+
Array.isArray(entry.hooks) &&
48+
entry.hooks.some((h: any) => h.command === hookCommand),
49+
)
50+
if (!alreadyExists) {
51+
settings.hooks.Stop.push({
52+
matcher: "",
53+
hooks: [{ type: "command", command: hookCommand }],
54+
})
55+
}
56+
57+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2))
58+
}
59+
60+
async function ensureValidationSetup(): Promise<void> {
61+
try {
62+
const claudeDir = getClaudeDir()
63+
const loggingEnabled = process.env.ALTIMATE_LOGGER_DISABLED !== "true"
64+
65+
// Always install /validate skill (SKILL.md + batch_validate.py)
66+
const validateSkillDir = path.join(claudeDir, "skills", "validate")
67+
await fs.mkdir(validateSkillDir, { recursive: true })
68+
await fs.writeFile(
69+
path.join(validateSkillDir, "SKILL.md"),
70+
await readAsset("ALTIMATE_VALIDATE_SKILL_MD", "../skill/validate/SKILL.md"),
71+
)
72+
await fs.writeFile(
73+
path.join(validateSkillDir, "batch_validate.py"),
74+
await readAsset("ALTIMATE_VALIDATE_BATCH_PY", "../skill/validate/batch_validate.py"),
75+
)
76+
77+
// Install hook + register in settings.json only when logging is enabled
78+
if (loggingEnabled) {
79+
const hooksDir = path.join(claudeDir, "hooks")
80+
await fs.mkdir(hooksDir, { recursive: true })
81+
const hookPath = path.join(hooksDir, "altimate_logger_hook.py")
82+
await fs.writeFile(
83+
hookPath,
84+
await readAsset("ALTIMATE_LOGGER_HOOK_PY", "../skill/validate/logger_hook.py"),
85+
)
86+
await mergeStopHook(
87+
path.join(claudeDir, "settings.json"),
88+
`uv run --with requests "${hookPath}"`,
89+
)
90+
}
91+
} catch {
92+
// Never block startup on setup failure
93+
}
94+
}
1695

1796
export async function InstanceBootstrap() {
1897
Log.Default.info("bootstrapping", { directory: Instance.directory })
98+
await ensureValidationSetup()
1999
await Plugin.init()
20100
ShareNext.init()
21101
Format.init()
@@ -34,4 +114,4 @@ export async function InstanceBootstrap() {
34114
await Project.setInitialized(Instance.project.id)
35115
}
36116
})
37-
}
117+
}

0 commit comments

Comments
 (0)