@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.
- package/LICENSE +674 -0
- package/README.md +445 -0
- package/package.json +46 -0
- package/src/agent/agent.mjs +170 -0
- package/src/agent/custom-agent-loader.mjs +158 -0
- package/src/agent/generator.mjs +115 -0
- package/src/agent/prompt/architect.txt +36 -0
- package/src/agent/prompt/build-fixer.txt +71 -0
- package/src/agent/prompt/build.txt +101 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +29 -0
- package/src/agent/prompt/guide.txt +40 -0
- package/src/agent/prompt/longagent.txt +178 -0
- package/src/agent/prompt/plan.txt +50 -0
- package/src/agent/prompt/researcher.txt +23 -0
- package/src/agent/prompt/reviewer.txt +44 -0
- package/src/agent/prompt/security-reviewer.txt +62 -0
- package/src/agent/prompt/tdd-guide.txt +84 -0
- package/src/agent/prompt/title.txt +8 -0
- package/src/command/custom-commands.mjs +57 -0
- package/src/commands/agent.mjs +71 -0
- package/src/commands/audit.mjs +77 -0
- package/src/commands/background.mjs +86 -0
- package/src/commands/chat.mjs +114 -0
- package/src/commands/command.mjs +41 -0
- package/src/commands/config.mjs +44 -0
- package/src/commands/doctor.mjs +148 -0
- package/src/commands/hook.mjs +29 -0
- package/src/commands/init.mjs +141 -0
- package/src/commands/longagent.mjs +100 -0
- package/src/commands/mcp.mjs +89 -0
- package/src/commands/permission.mjs +36 -0
- package/src/commands/prompt.mjs +42 -0
- package/src/commands/review.mjs +266 -0
- package/src/commands/rule.mjs +34 -0
- package/src/commands/session.mjs +235 -0
- package/src/commands/theme.mjs +98 -0
- package/src/commands/usage.mjs +91 -0
- package/src/config/defaults.mjs +195 -0
- package/src/config/import-config.mjs +76 -0
- package/src/config/load-config.mjs +76 -0
- package/src/config/schema.mjs +509 -0
- package/src/context.mjs +40 -0
- package/src/core/constants.mjs +46 -0
- package/src/core/errors.mjs +57 -0
- package/src/core/events.mjs +29 -0
- package/src/core/types.mjs +57 -0
- package/src/github/api.mjs +78 -0
- package/src/github/auth.mjs +286 -0
- package/src/github/flow.mjs +298 -0
- package/src/github/workspace.mjs +212 -0
- package/src/index.mjs +82 -0
- package/src/knowledge/api-design.txt +9 -0
- package/src/knowledge/cpp.txt +10 -0
- package/src/knowledge/docker.txt +10 -0
- package/src/knowledge/dotnet.txt +9 -0
- package/src/knowledge/electron.txt +10 -0
- package/src/knowledge/flutter.txt +10 -0
- package/src/knowledge/go.txt +9 -0
- package/src/knowledge/graphql.txt +10 -0
- package/src/knowledge/java.txt +9 -0
- package/src/knowledge/kotlin.txt +10 -0
- package/src/knowledge/loader.mjs +125 -0
- package/src/knowledge/next.txt +8 -0
- package/src/knowledge/node.txt +8 -0
- package/src/knowledge/nuxt.txt +9 -0
- package/src/knowledge/php.txt +10 -0
- package/src/knowledge/python.txt +10 -0
- package/src/knowledge/react-native.txt +10 -0
- package/src/knowledge/react.txt +9 -0
- package/src/knowledge/ruby.txt +11 -0
- package/src/knowledge/rust.txt +9 -0
- package/src/knowledge/svelte.txt +9 -0
- package/src/knowledge/swift.txt +10 -0
- package/src/knowledge/tailwind.txt +10 -0
- package/src/knowledge/testing.txt +8 -0
- package/src/knowledge/typescript.txt +8 -0
- package/src/knowledge/vue.txt +9 -0
- package/src/mcp/client-http.mjs +157 -0
- package/src/mcp/client-sse.mjs +286 -0
- package/src/mcp/client-stdio.mjs +451 -0
- package/src/mcp/registry.mjs +394 -0
- package/src/mcp/stdio-framing.mjs +127 -0
- package/src/orchestration/background-manager.mjs +358 -0
- package/src/orchestration/background-worker.mjs +245 -0
- package/src/orchestration/longagent-manager.mjs +116 -0
- package/src/orchestration/stage-scheduler.mjs +489 -0
- package/src/orchestration/subagent-router.mjs +62 -0
- package/src/orchestration/task-scheduler.mjs +74 -0
- package/src/permission/engine.mjs +92 -0
- package/src/permission/exec-policy.mjs +372 -0
- package/src/permission/prompt.mjs +39 -0
- package/src/permission/rules.mjs +120 -0
- package/src/permission/workspace-trust.mjs +44 -0
- package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
- package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
- package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
- package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
- package/src/plugin/hook-bus.mjs +154 -0
- package/src/provider/anthropic.mjs +389 -0
- package/src/provider/ollama.mjs +236 -0
- package/src/provider/openai-compatible.mjs +1 -0
- package/src/provider/openai.mjs +339 -0
- package/src/provider/retry-policy.mjs +68 -0
- package/src/provider/router.mjs +228 -0
- package/src/provider/sse.mjs +91 -0
- package/src/repl.mjs +2929 -0
- package/src/review/diff-parser.mjs +36 -0
- package/src/review/rejection-queue.mjs +62 -0
- package/src/review/review-store.mjs +21 -0
- package/src/review/risk-score.mjs +61 -0
- package/src/rules/load-rules.mjs +64 -0
- package/src/runtime.mjs +1 -0
- package/src/session/checkpoint.mjs +239 -0
- package/src/session/compaction.mjs +276 -0
- package/src/session/engine.mjs +225 -0
- package/src/session/instinct-manager.mjs +172 -0
- package/src/session/instruction-loader.mjs +25 -0
- package/src/session/longagent-plan.mjs +329 -0
- package/src/session/longagent-scaffold.mjs +100 -0
- package/src/session/longagent.mjs +1462 -0
- package/src/session/loop.mjs +905 -0
- package/src/session/memory-loader.mjs +75 -0
- package/src/session/project-context.mjs +367 -0
- package/src/session/prompt/anthropic.txt +151 -0
- package/src/session/prompt/beast.txt +37 -0
- package/src/session/prompt/max-steps.txt +6 -0
- package/src/session/prompt/plan.txt +9 -0
- package/src/session/prompt/qwen.txt +46 -0
- package/src/session/prompt-loader.mjs +18 -0
- package/src/session/recovery.mjs +52 -0
- package/src/session/store.mjs +503 -0
- package/src/session/system-prompt.mjs +260 -0
- package/src/session/task-validator.mjs +266 -0
- package/src/session/usability-gates.mjs +379 -0
- package/src/skill/builtin/backend-patterns.mjs +123 -0
- package/src/skill/builtin/commit.mjs +64 -0
- package/src/skill/builtin/debug.mjs +45 -0
- package/src/skill/builtin/frontend-patterns.mjs +120 -0
- package/src/skill/builtin/frontend.mjs +188 -0
- package/src/skill/builtin/init.mjs +220 -0
- package/src/skill/builtin/review.mjs +49 -0
- package/src/skill/builtin/security-checklist.mjs +80 -0
- package/src/skill/builtin/tdd.mjs +54 -0
- package/src/skill/generator.mjs +113 -0
- package/src/skill/registry.mjs +336 -0
- package/src/storage/audit-store.mjs +83 -0
- package/src/storage/event-log.mjs +82 -0
- package/src/storage/ghost-commit-store.mjs +235 -0
- package/src/storage/json-store.mjs +53 -0
- package/src/storage/paths.mjs +148 -0
- package/src/theme/color.mjs +64 -0
- package/src/theme/default-theme.mjs +29 -0
- package/src/theme/load-theme.mjs +71 -0
- package/src/theme/markdown.mjs +135 -0
- package/src/theme/schema.mjs +45 -0
- package/src/theme/status-bar.mjs +158 -0
- package/src/tool/audit-wrapper.mjs +38 -0
- package/src/tool/edit-transaction.mjs +126 -0
- package/src/tool/executor.mjs +109 -0
- package/src/tool/file-lock-manager.mjs +85 -0
- package/src/tool/git-auto.mjs +545 -0
- package/src/tool/git-full-auto.mjs +478 -0
- package/src/tool/image-util.mjs +276 -0
- package/src/tool/prompt/background_cancel.txt +1 -0
- package/src/tool/prompt/background_output.txt +1 -0
- package/src/tool/prompt/bash.txt +71 -0
- package/src/tool/prompt/codesearch.txt +18 -0
- package/src/tool/prompt/edit.txt +27 -0
- package/src/tool/prompt/enter_plan.txt +74 -0
- package/src/tool/prompt/exit_plan.txt +62 -0
- package/src/tool/prompt/glob.txt +33 -0
- package/src/tool/prompt/grep.txt +43 -0
- package/src/tool/prompt/list.txt +8 -0
- package/src/tool/prompt/multiedit.txt +20 -0
- package/src/tool/prompt/notebookedit.txt +21 -0
- package/src/tool/prompt/patch.txt +24 -0
- package/src/tool/prompt/question.txt +44 -0
- package/src/tool/prompt/read.txt +40 -0
- package/src/tool/prompt/task.txt +83 -0
- package/src/tool/prompt/todowrite.txt +117 -0
- package/src/tool/prompt/webfetch.txt +38 -0
- package/src/tool/prompt/websearch.txt +43 -0
- package/src/tool/prompt/write.txt +38 -0
- package/src/tool/prompt-loader.mjs +18 -0
- package/src/tool/question-prompt.mjs +86 -0
- package/src/tool/registry.mjs +1309 -0
- package/src/tool/task-tool.mjs +28 -0
- package/src/ui/activity-renderer.mjs +410 -0
- package/src/ui/repl-dashboard.mjs +357 -0
- package/src/usage/pricing.mjs +121 -0
- package/src/usage/usage-meter.mjs +113 -0
- package/src/util/git.mjs +496 -0
- package/src/util/template.mjs +10 -0
- 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
|
+
}
|