@kkelly-offical/kkcode 0.1.2

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.
Files changed (196) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +445 -0
  3. package/package.json +46 -0
  4. package/src/agent/agent.mjs +170 -0
  5. package/src/agent/custom-agent-loader.mjs +158 -0
  6. package/src/agent/generator.mjs +115 -0
  7. package/src/agent/prompt/architect.txt +36 -0
  8. package/src/agent/prompt/build-fixer.txt +71 -0
  9. package/src/agent/prompt/build.txt +101 -0
  10. package/src/agent/prompt/compaction.txt +12 -0
  11. package/src/agent/prompt/explore.txt +29 -0
  12. package/src/agent/prompt/guide.txt +40 -0
  13. package/src/agent/prompt/longagent.txt +178 -0
  14. package/src/agent/prompt/plan.txt +50 -0
  15. package/src/agent/prompt/researcher.txt +23 -0
  16. package/src/agent/prompt/reviewer.txt +44 -0
  17. package/src/agent/prompt/security-reviewer.txt +62 -0
  18. package/src/agent/prompt/tdd-guide.txt +84 -0
  19. package/src/agent/prompt/title.txt +8 -0
  20. package/src/command/custom-commands.mjs +57 -0
  21. package/src/commands/agent.mjs +71 -0
  22. package/src/commands/audit.mjs +77 -0
  23. package/src/commands/background.mjs +86 -0
  24. package/src/commands/chat.mjs +114 -0
  25. package/src/commands/command.mjs +41 -0
  26. package/src/commands/config.mjs +44 -0
  27. package/src/commands/doctor.mjs +148 -0
  28. package/src/commands/hook.mjs +29 -0
  29. package/src/commands/init.mjs +141 -0
  30. package/src/commands/longagent.mjs +100 -0
  31. package/src/commands/mcp.mjs +89 -0
  32. package/src/commands/permission.mjs +36 -0
  33. package/src/commands/prompt.mjs +42 -0
  34. package/src/commands/review.mjs +266 -0
  35. package/src/commands/rule.mjs +34 -0
  36. package/src/commands/session.mjs +235 -0
  37. package/src/commands/theme.mjs +98 -0
  38. package/src/commands/usage.mjs +91 -0
  39. package/src/config/defaults.mjs +195 -0
  40. package/src/config/import-config.mjs +76 -0
  41. package/src/config/load-config.mjs +76 -0
  42. package/src/config/schema.mjs +509 -0
  43. package/src/context.mjs +40 -0
  44. package/src/core/constants.mjs +46 -0
  45. package/src/core/errors.mjs +57 -0
  46. package/src/core/events.mjs +29 -0
  47. package/src/core/types.mjs +57 -0
  48. package/src/github/api.mjs +78 -0
  49. package/src/github/auth.mjs +286 -0
  50. package/src/github/flow.mjs +298 -0
  51. package/src/github/workspace.mjs +212 -0
  52. package/src/index.mjs +82 -0
  53. package/src/knowledge/api-design.txt +9 -0
  54. package/src/knowledge/cpp.txt +10 -0
  55. package/src/knowledge/docker.txt +10 -0
  56. package/src/knowledge/dotnet.txt +9 -0
  57. package/src/knowledge/electron.txt +10 -0
  58. package/src/knowledge/flutter.txt +10 -0
  59. package/src/knowledge/go.txt +9 -0
  60. package/src/knowledge/graphql.txt +10 -0
  61. package/src/knowledge/java.txt +9 -0
  62. package/src/knowledge/kotlin.txt +10 -0
  63. package/src/knowledge/loader.mjs +125 -0
  64. package/src/knowledge/next.txt +8 -0
  65. package/src/knowledge/node.txt +8 -0
  66. package/src/knowledge/nuxt.txt +9 -0
  67. package/src/knowledge/php.txt +10 -0
  68. package/src/knowledge/python.txt +10 -0
  69. package/src/knowledge/react-native.txt +10 -0
  70. package/src/knowledge/react.txt +9 -0
  71. package/src/knowledge/ruby.txt +11 -0
  72. package/src/knowledge/rust.txt +9 -0
  73. package/src/knowledge/svelte.txt +9 -0
  74. package/src/knowledge/swift.txt +10 -0
  75. package/src/knowledge/tailwind.txt +10 -0
  76. package/src/knowledge/testing.txt +8 -0
  77. package/src/knowledge/typescript.txt +8 -0
  78. package/src/knowledge/vue.txt +9 -0
  79. package/src/mcp/client-http.mjs +157 -0
  80. package/src/mcp/client-sse.mjs +286 -0
  81. package/src/mcp/client-stdio.mjs +451 -0
  82. package/src/mcp/registry.mjs +394 -0
  83. package/src/mcp/stdio-framing.mjs +127 -0
  84. package/src/orchestration/background-manager.mjs +358 -0
  85. package/src/orchestration/background-worker.mjs +245 -0
  86. package/src/orchestration/longagent-manager.mjs +116 -0
  87. package/src/orchestration/stage-scheduler.mjs +489 -0
  88. package/src/orchestration/subagent-router.mjs +62 -0
  89. package/src/orchestration/task-scheduler.mjs +74 -0
  90. package/src/permission/engine.mjs +92 -0
  91. package/src/permission/exec-policy.mjs +372 -0
  92. package/src/permission/prompt.mjs +39 -0
  93. package/src/permission/rules.mjs +120 -0
  94. package/src/permission/workspace-trust.mjs +44 -0
  95. package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
  96. package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
  97. package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
  98. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
  99. package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
  100. package/src/plugin/hook-bus.mjs +154 -0
  101. package/src/provider/anthropic.mjs +389 -0
  102. package/src/provider/ollama.mjs +236 -0
  103. package/src/provider/openai-compatible.mjs +1 -0
  104. package/src/provider/openai.mjs +339 -0
  105. package/src/provider/retry-policy.mjs +68 -0
  106. package/src/provider/router.mjs +228 -0
  107. package/src/provider/sse.mjs +91 -0
  108. package/src/repl.mjs +2929 -0
  109. package/src/review/diff-parser.mjs +36 -0
  110. package/src/review/rejection-queue.mjs +62 -0
  111. package/src/review/review-store.mjs +21 -0
  112. package/src/review/risk-score.mjs +61 -0
  113. package/src/rules/load-rules.mjs +64 -0
  114. package/src/runtime.mjs +1 -0
  115. package/src/session/checkpoint.mjs +239 -0
  116. package/src/session/compaction.mjs +276 -0
  117. package/src/session/engine.mjs +225 -0
  118. package/src/session/instinct-manager.mjs +172 -0
  119. package/src/session/instruction-loader.mjs +25 -0
  120. package/src/session/longagent-plan.mjs +329 -0
  121. package/src/session/longagent-scaffold.mjs +100 -0
  122. package/src/session/longagent.mjs +1462 -0
  123. package/src/session/loop.mjs +905 -0
  124. package/src/session/memory-loader.mjs +75 -0
  125. package/src/session/project-context.mjs +367 -0
  126. package/src/session/prompt/anthropic.txt +151 -0
  127. package/src/session/prompt/beast.txt +37 -0
  128. package/src/session/prompt/max-steps.txt +6 -0
  129. package/src/session/prompt/plan.txt +9 -0
  130. package/src/session/prompt/qwen.txt +46 -0
  131. package/src/session/prompt-loader.mjs +18 -0
  132. package/src/session/recovery.mjs +52 -0
  133. package/src/session/store.mjs +503 -0
  134. package/src/session/system-prompt.mjs +260 -0
  135. package/src/session/task-validator.mjs +266 -0
  136. package/src/session/usability-gates.mjs +379 -0
  137. package/src/skill/builtin/backend-patterns.mjs +123 -0
  138. package/src/skill/builtin/commit.mjs +64 -0
  139. package/src/skill/builtin/debug.mjs +45 -0
  140. package/src/skill/builtin/frontend-patterns.mjs +120 -0
  141. package/src/skill/builtin/frontend.mjs +188 -0
  142. package/src/skill/builtin/init.mjs +220 -0
  143. package/src/skill/builtin/review.mjs +49 -0
  144. package/src/skill/builtin/security-checklist.mjs +80 -0
  145. package/src/skill/builtin/tdd.mjs +54 -0
  146. package/src/skill/generator.mjs +113 -0
  147. package/src/skill/registry.mjs +336 -0
  148. package/src/storage/audit-store.mjs +83 -0
  149. package/src/storage/event-log.mjs +82 -0
  150. package/src/storage/ghost-commit-store.mjs +235 -0
  151. package/src/storage/json-store.mjs +53 -0
  152. package/src/storage/paths.mjs +148 -0
  153. package/src/theme/color.mjs +64 -0
  154. package/src/theme/default-theme.mjs +29 -0
  155. package/src/theme/load-theme.mjs +71 -0
  156. package/src/theme/markdown.mjs +135 -0
  157. package/src/theme/schema.mjs +45 -0
  158. package/src/theme/status-bar.mjs +158 -0
  159. package/src/tool/audit-wrapper.mjs +38 -0
  160. package/src/tool/edit-transaction.mjs +126 -0
  161. package/src/tool/executor.mjs +109 -0
  162. package/src/tool/file-lock-manager.mjs +85 -0
  163. package/src/tool/git-auto.mjs +545 -0
  164. package/src/tool/git-full-auto.mjs +478 -0
  165. package/src/tool/image-util.mjs +276 -0
  166. package/src/tool/prompt/background_cancel.txt +1 -0
  167. package/src/tool/prompt/background_output.txt +1 -0
  168. package/src/tool/prompt/bash.txt +71 -0
  169. package/src/tool/prompt/codesearch.txt +18 -0
  170. package/src/tool/prompt/edit.txt +27 -0
  171. package/src/tool/prompt/enter_plan.txt +74 -0
  172. package/src/tool/prompt/exit_plan.txt +62 -0
  173. package/src/tool/prompt/glob.txt +33 -0
  174. package/src/tool/prompt/grep.txt +43 -0
  175. package/src/tool/prompt/list.txt +8 -0
  176. package/src/tool/prompt/multiedit.txt +20 -0
  177. package/src/tool/prompt/notebookedit.txt +21 -0
  178. package/src/tool/prompt/patch.txt +24 -0
  179. package/src/tool/prompt/question.txt +44 -0
  180. package/src/tool/prompt/read.txt +40 -0
  181. package/src/tool/prompt/task.txt +83 -0
  182. package/src/tool/prompt/todowrite.txt +117 -0
  183. package/src/tool/prompt/webfetch.txt +38 -0
  184. package/src/tool/prompt/websearch.txt +43 -0
  185. package/src/tool/prompt/write.txt +38 -0
  186. package/src/tool/prompt-loader.mjs +18 -0
  187. package/src/tool/question-prompt.mjs +86 -0
  188. package/src/tool/registry.mjs +1309 -0
  189. package/src/tool/task-tool.mjs +28 -0
  190. package/src/ui/activity-renderer.mjs +410 -0
  191. package/src/ui/repl-dashboard.mjs +357 -0
  192. package/src/usage/pricing.mjs +121 -0
  193. package/src/usage/usage-meter.mjs +113 -0
  194. package/src/util/git.mjs +496 -0
  195. package/src/util/template.mjs +10 -0
  196. package/src/util/yaml.mjs +100 -0
@@ -0,0 +1,41 @@
1
+ // Console.log warning hook
2
+ // Warns when console.log is present in edited production files
3
+
4
+ import { readFile } from "node:fs/promises"
5
+ import path from "node:path"
6
+
7
+ const PRODUCTION_FILE = /\.(js|jsx|ts|tsx|mjs|cjs)$/
8
+ const IGNORE_PATH = /(test|spec|__tests__|__mocks__|\.test\.|\.spec\.|\.config\.)/i
9
+
10
+ export default {
11
+ name: "console-warn",
12
+ tool: {
13
+ async after(payload) {
14
+ const { toolName, args, result, cwd } = payload
15
+ if (!["edit", "write"].includes(toolName)) return payload
16
+
17
+ const file = args?.path
18
+ if (!file) return payload
19
+ if (!PRODUCTION_FILE.test(file)) return payload
20
+ if (IGNORE_PATH.test(file)) return payload
21
+
22
+ const target = path.resolve(cwd || process.cwd(), file)
23
+ try {
24
+ const content = await readFile(target, "utf8")
25
+ const matches = content.match(/console\.(log|debug|info)\(/g)
26
+ if (matches && matches.length > 0) {
27
+ const warning = `\n⚠ Found ${matches.length} console.log/debug/info call(s) in ${file}. Consider removing before production.`
28
+ if (typeof result === "string") {
29
+ payload.result = result + warning
30
+ } else if (result && typeof result === "object") {
31
+ payload.result = { ...result, output: (result.output || "") + warning }
32
+ }
33
+ }
34
+ } catch {
35
+ // File not readable, skip
36
+ }
37
+
38
+ return payload
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,75 @@
1
+ // Extract patterns hook
2
+ // Before compaction, analyzes the conversation for repeatable patterns and saves as instincts
3
+
4
+ import { addInstinct } from "../../session/instinct-manager.mjs"
5
+
6
+ // Patterns we look for in tool usage sequences
7
+ const PATTERN_SIGNALS = [
8
+ { regex: /always run.*test/i, pattern: "Always run tests after code changes", category: "workflow" },
9
+ { regex: /npm audit|pip audit|cargo audit/i, pattern: "Run dependency audit after adding packages", category: "security" },
10
+ { regex: /git add.*&&.*git commit/i, pattern: "Stage specific files rather than using git add -A", category: "workflow" },
11
+ { regex: /\.test\.(ts|js|tsx|jsx)|_test\.go|test_.*\.py/i, pattern: "Co-locate test files with implementation", category: "testing" },
12
+ { regex: /prettier|eslint --fix|black |ruff /i, pattern: "Format code after editing", category: "coding" },
13
+ { regex: /tsconfig|tsc --noEmit/i, pattern: "Verify TypeScript types after changes", category: "coding" }
14
+ ]
15
+
16
+ export default {
17
+ name: "extract-patterns",
18
+ session: {
19
+ async compacting(payload) {
20
+ const { messages, cwd } = payload
21
+ if (!messages || !Array.isArray(messages) || !cwd) return payload
22
+
23
+ // Scan assistant messages for pattern signals
24
+ const textContent = messages
25
+ .filter((m) => m.role === "assistant")
26
+ .map((m) => {
27
+ if (typeof m.content === "string") return m.content
28
+ if (Array.isArray(m.content)) {
29
+ return m.content
30
+ .filter((b) => b.type === "text")
31
+ .map((b) => b.text)
32
+ .join(" ")
33
+ }
34
+ return ""
35
+ })
36
+ .join("\n")
37
+
38
+ for (const signal of PATTERN_SIGNALS) {
39
+ if (signal.regex.test(textContent)) {
40
+ try {
41
+ await addInstinct(cwd, signal.pattern, signal.category)
42
+ } catch {
43
+ // Non-critical — skip on failure
44
+ }
45
+ }
46
+ }
47
+
48
+ // Look for user-confirmed patterns ("always do X", "never do Y", "remember to Z")
49
+ const userText = messages
50
+ .filter((m) => m.role === "user")
51
+ .map((m) => (typeof m.content === "string" ? m.content : ""))
52
+ .join("\n")
53
+
54
+ const alwaysMatch = userText.match(/(?:always|每次都要|总是)\s+(.{10,80})/gi)
55
+ if (alwaysMatch) {
56
+ for (const match of alwaysMatch.slice(0, 3)) {
57
+ try {
58
+ await addInstinct(cwd, match.trim(), "workflow")
59
+ } catch { /* skip */ }
60
+ }
61
+ }
62
+
63
+ const neverMatch = userText.match(/(?:never|不要|禁止)\s+(.{10,80})/gi)
64
+ if (neverMatch) {
65
+ for (const match of neverMatch.slice(0, 3)) {
66
+ try {
67
+ await addInstinct(cwd, match.trim(), "workflow")
68
+ } catch { /* skip */ }
69
+ }
70
+ }
71
+
72
+ return payload
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,57 @@
1
+ // Post-edit auto-format hook
2
+ // Runs prettier on JS/TS/CSS/JSON files after edit, if prettier is installed
3
+
4
+ import { exec as execCb } from "node:child_process"
5
+ import { promisify } from "node:util"
6
+ import { access } from "node:fs/promises"
7
+ import path from "node:path"
8
+
9
+ const exec = promisify(execCb)
10
+
11
+ const FORMATTABLE = /\.(js|jsx|ts|tsx|css|scss|less|json|md|yaml|yml|html|vue|svelte)$/
12
+
13
+ async function fileExists(p) {
14
+ try { await access(p); return true } catch { return false }
15
+ }
16
+
17
+ export default {
18
+ name: "post-edit-format",
19
+ tool: {
20
+ async after(payload) {
21
+ const { toolName, args, cwd } = payload
22
+ if (!["edit", "write", "multiedit"].includes(toolName)) return payload
23
+
24
+ // Collect affected files
25
+ const files = []
26
+ if (args?.path) files.push(args.path)
27
+ if (args?.changes) {
28
+ for (const c of args.changes) {
29
+ if (c.path) files.push(c.path)
30
+ }
31
+ }
32
+
33
+ const formattable = files.filter(f => FORMATTABLE.test(f))
34
+ if (formattable.length === 0) return payload
35
+
36
+ const root = cwd || process.cwd()
37
+
38
+ // Check if prettier is available (package.json devDependency or global)
39
+ const pkgPath = path.join(root, "node_modules", ".bin", "prettier")
40
+ if (!(await fileExists(pkgPath))) return payload
41
+
42
+ for (const file of formattable) {
43
+ const target = path.resolve(root, file)
44
+ try {
45
+ await exec(`npx prettier --write "${target}"`, {
46
+ cwd: root,
47
+ timeout: 10000
48
+ })
49
+ } catch {
50
+ // Formatting failure is non-critical, silently skip
51
+ }
52
+ }
53
+
54
+ return payload
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,61 @@
1
+ // Post-edit TypeScript type check hook
2
+ // Runs `tsc --noEmit` after editing .ts/.tsx files to catch type errors early
3
+
4
+ import { exec as execCb } from "node:child_process"
5
+ import { promisify } from "node:util"
6
+ import { access } from "node:fs/promises"
7
+ import path from "node:path"
8
+
9
+ const exec = promisify(execCb)
10
+
11
+ async function fileExists(p) {
12
+ try { await access(p); return true } catch { return false }
13
+ }
14
+
15
+ export default {
16
+ name: "post-edit-typecheck",
17
+ tool: {
18
+ async after(payload) {
19
+ const { toolName, args, result, cwd } = payload
20
+ if (!["edit", "write", "multiedit"].includes(toolName)) return payload
21
+
22
+ // Determine affected files
23
+ const files = []
24
+ if (args?.path) files.push(args.path)
25
+ if (args?.changes) {
26
+ for (const c of args.changes) {
27
+ if (c.path) files.push(c.path)
28
+ }
29
+ }
30
+
31
+ // Only check if at least one TS/TSX file was edited
32
+ const tsFiles = files.filter(f => /\.tsx?$/.test(f))
33
+ if (tsFiles.length === 0) return payload
34
+
35
+ // Verify tsconfig.json exists in project
36
+ const tsconfigPath = path.join(cwd || process.cwd(), "tsconfig.json")
37
+ if (!(await fileExists(tsconfigPath))) return payload
38
+
39
+ try {
40
+ await exec("npx tsc --noEmit --pretty 2>&1", {
41
+ cwd: cwd || process.cwd(),
42
+ timeout: 15000
43
+ })
44
+ // No errors — silently pass through
45
+ } catch (error) {
46
+ const output = (error.stdout || error.stderr || "").trim()
47
+ if (output) {
48
+ // Append type check warnings to tool result
49
+ const warning = `\n⚠ TypeScript type check found issues:\n${output.slice(0, 2000)}`
50
+ if (typeof result === "string") {
51
+ payload.result = result + warning
52
+ } else if (result && typeof result === "object") {
53
+ payload.result = { ...result, output: (result.output || "") + warning }
54
+ }
55
+ }
56
+ }
57
+
58
+ return payload
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,38 @@
1
+ // Strategic compaction suggestion hook
2
+ // Suggests /compact at logical breakpoints instead of waiting for automatic compaction
3
+
4
+ let toolCallCount = 0
5
+ let lastSuggestionAt = 0
6
+ const THRESHOLD = 30 // suggest every ~30 tool calls
7
+
8
+ export default {
9
+ name: "strategic-compaction",
10
+ tool: {
11
+ async after(payload) {
12
+ toolCallCount++
13
+
14
+ // Only suggest periodically
15
+ if (toolCallCount - lastSuggestionAt < THRESHOLD) return payload
16
+
17
+ const { toolName, result } = payload
18
+
19
+ // Detect logical breakpoints: after research phases, after build/test, after multi-file edits
20
+ const isResearchEnd = toolName === "grep" || toolName === "glob" || toolName === "read"
21
+ const isBuildEnd = toolName === "bash"
22
+ const isEditEnd = toolName === "edit" || toolName === "write" || toolName === "multiedit"
23
+
24
+ if (!isResearchEnd && !isBuildEnd && !isEditEnd) return payload
25
+
26
+ lastSuggestionAt = toolCallCount
27
+ const suggestion = `\n💡 You've made ${toolCallCount} tool calls in this session. Consider running /compact to free up context space if the conversation is getting long.`
28
+
29
+ if (typeof result === "string") {
30
+ payload.result = result + suggestion
31
+ } else if (result && typeof result === "object") {
32
+ payload.result = { ...result, output: (result.output || "") + suggestion }
33
+ }
34
+
35
+ return payload
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,154 @@
1
+ import path from "node:path"
2
+ import { access, readdir } from "node:fs/promises"
3
+ import { pathToFileURL, fileURLToPath } from "node:url"
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
6
+
7
+ const HOOK_EVENTS = [
8
+ "chat.params",
9
+ "chat.message",
10
+ "messages.transform",
11
+ "tool.before",
12
+ "tool.after",
13
+ "event",
14
+ "session.compacting"
15
+ ]
16
+
17
+ const state = {
18
+ loaded: false,
19
+ hooks: [],
20
+ errors: []
21
+ }
22
+
23
+ function normalizeHook(mod, source) {
24
+ const hook = mod.default || mod
25
+ if (!hook || typeof hook !== "object") return null
26
+ return {
27
+ source,
28
+ name: hook.name || path.basename(source),
29
+ chat: hook.chat || {},
30
+ tool: hook.tool || {},
31
+ event: typeof hook.event === "function" ? hook.event : null,
32
+ session: hook.session || {}
33
+ }
34
+ }
35
+
36
+ async function exists(target) {
37
+ try {
38
+ await access(target)
39
+ return true
40
+ } catch {
41
+ return false
42
+ }
43
+ }
44
+
45
+ async function discover(dir) {
46
+ if (!(await exists(dir))) return []
47
+ const entries = await readdir(dir, { withFileTypes: true })
48
+ return entries
49
+ .filter((entry) => entry.isFile() && [".mjs", ".js"].includes(path.extname(entry.name).toLowerCase()))
50
+ .map((entry) => path.join(dir, entry.name))
51
+ }
52
+
53
+ async function loadModule(file) {
54
+ try {
55
+ const mod = await import(pathToFileURL(file).href)
56
+ return { hook: normalizeHook(mod, file), error: null }
57
+ } catch (error) {
58
+ return { hook: null, error: `${file}: ${error.message}` }
59
+ }
60
+ }
61
+
62
+ export async function initHookBus(cwd = process.cwd()) {
63
+ if (state.loaded) return state
64
+ // Built-in hooks ship with kkcode (lowest priority — user hooks can override)
65
+ const builtinHooks = path.join(__dirname, "builtin-hooks")
66
+ const userRoot = process.env.USERPROFILE || process.env.HOME || cwd
67
+ const userHooks = path.join(userRoot, ".kkcode", "hooks")
68
+ const projectHooks = path.join(cwd, ".kkcode", "hooks")
69
+ // Load order: builtin → user → project (later hooks in chain take priority)
70
+ const files = [...(await discover(builtinHooks)), ...(await discover(userHooks)), ...(await discover(projectHooks))]
71
+ for (const file of files) {
72
+ const loaded = await loadModule(file)
73
+ if (loaded.error) {
74
+ state.errors.push(loaded.error)
75
+ continue
76
+ }
77
+ if (loaded.hook) state.hooks.push(loaded.hook)
78
+ }
79
+ state.loaded = true
80
+ return state
81
+ }
82
+
83
+ async function applyTransformChain(initial, chain) {
84
+ let current = initial
85
+ for (const fn of chain) {
86
+ const next = await fn(current)
87
+ if (next !== undefined) current = next
88
+ }
89
+ return current
90
+ }
91
+
92
+ export const HookBus = {
93
+ supportedEvents() {
94
+ return [...HOOK_EVENTS]
95
+ },
96
+ list() {
97
+ return state.hooks.map((hook) => ({ name: hook.name, source: hook.source }))
98
+ },
99
+ errors() {
100
+ return [...state.errors]
101
+ },
102
+ async chatParams(payload) {
103
+ const chain = state.hooks
104
+ .map((hook) => hook.chat?.params)
105
+ .filter((fn) => typeof fn === "function")
106
+ .map((fn) => async (current) => fn(current))
107
+ return applyTransformChain(payload, chain)
108
+ },
109
+ async chatMessage(payload) {
110
+ const chain = state.hooks
111
+ .map((hook) => hook.chat?.message)
112
+ .filter((fn) => typeof fn === "function")
113
+ .map((fn) => async (current) => fn(current))
114
+ return applyTransformChain(payload, chain)
115
+ },
116
+ async messagesTransform(payload) {
117
+ const chain = state.hooks
118
+ .map((hook) => hook.chat?.messagesTransform)
119
+ .filter((fn) => typeof fn === "function")
120
+ .map((fn) => async (current) => fn(current))
121
+ return applyTransformChain(payload, chain)
122
+ },
123
+ async toolBefore(payload) {
124
+ const chain = state.hooks
125
+ .map((hook) => hook.tool?.before)
126
+ .filter((fn) => typeof fn === "function")
127
+ .map((fn) => async (current) => fn(current))
128
+ return applyTransformChain(payload, chain)
129
+ },
130
+ async toolAfter(payload) {
131
+ const chain = state.hooks
132
+ .map((hook) => hook.tool?.after)
133
+ .filter((fn) => typeof fn === "function")
134
+ .map((fn) => async (current) => fn(current))
135
+ return applyTransformChain(payload, chain)
136
+ },
137
+ async emit(eventType, payload) {
138
+ for (const hook of state.hooks) {
139
+ if (!hook.event) continue
140
+ try {
141
+ await hook.event({ type: eventType, payload })
142
+ } catch (err) {
143
+ console.error(`[hook-bus] emit error in ${hook.name}:`, err?.message || err)
144
+ }
145
+ }
146
+ },
147
+ async sessionCompacting(payload) {
148
+ const chain = state.hooks
149
+ .map((hook) => hook.session?.compacting)
150
+ .filter((fn) => typeof fn === "function")
151
+ .map((fn) => async (current) => fn(current))
152
+ return applyTransformChain(payload, chain)
153
+ }
154
+ }