@kkelly-offical/kkcode 0.1.7 → 0.2.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/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +228 -220
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +89 -89
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2981
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +298 -298
- package/src/session/engine.mjs +417 -232
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1097
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -900
- package/src/session/loop.mjs +1005 -930
- package/src/session/prompt/agent.txt +25 -25
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +31 -31
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +196 -195
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -519
- package/src/session/system-prompt.mjs +308 -273
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +99 -93
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
function globToRegex(pattern) {
|
|
2
|
+
let src = ""
|
|
3
|
+
let i = 0
|
|
4
|
+
while (i < pattern.length) {
|
|
5
|
+
const ch = pattern[i]
|
|
6
|
+
if (ch === "*" && pattern[i + 1] === "*") {
|
|
7
|
+
src += ".*"
|
|
8
|
+
i += 2
|
|
9
|
+
if (pattern[i] === "/") i++
|
|
10
|
+
} else if (ch === "*") {
|
|
11
|
+
src += "[^/]*"
|
|
12
|
+
i++
|
|
13
|
+
} else if (ch === "?") {
|
|
14
|
+
src += "[^/]"
|
|
15
|
+
i++
|
|
16
|
+
} else if (".+^${}()|[]\\".includes(ch)) {
|
|
17
|
+
src += `\\${ch}`
|
|
18
|
+
i++
|
|
19
|
+
} else {
|
|
20
|
+
src += ch
|
|
21
|
+
i++
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return new RegExp(`^${src}$`, "i")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizePath(value) {
|
|
28
|
+
return String(value || "")
|
|
29
|
+
.replace(/\\/g, "/")
|
|
30
|
+
.split("/")
|
|
31
|
+
.reduce((acc, segment) => {
|
|
32
|
+
if (!segment || segment === ".") return acc
|
|
33
|
+
if (segment === "..") {
|
|
34
|
+
acc.pop()
|
|
35
|
+
return acc
|
|
36
|
+
}
|
|
37
|
+
acc.push(segment)
|
|
38
|
+
return acc
|
|
39
|
+
}, [])
|
|
40
|
+
.join("/")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function matchGlob(pattern, value) {
|
|
44
|
+
return globToRegex(pattern).test(normalizePath(value))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_SENSITIVE_FILE_PATTERNS = [
|
|
48
|
+
"AGENTS.md",
|
|
49
|
+
"**/AGENTS.md",
|
|
50
|
+
"KKCODE.md",
|
|
51
|
+
"**/KKCODE.md",
|
|
52
|
+
".kkcode/**",
|
|
53
|
+
"**/.kkcode/**",
|
|
54
|
+
"kkcode.config.yaml",
|
|
55
|
+
"**/kkcode.config.yaml",
|
|
56
|
+
".mcp.json",
|
|
57
|
+
"**/.mcp.json",
|
|
58
|
+
".env",
|
|
59
|
+
".env.*",
|
|
60
|
+
"**/.env",
|
|
61
|
+
"**/.env.*",
|
|
62
|
+
".github/workflows/**",
|
|
63
|
+
"**/.github/workflows/**"
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const SENSITIVE_EDIT_TOOLS = new Set([
|
|
67
|
+
"write",
|
|
68
|
+
"edit",
|
|
69
|
+
"patch",
|
|
70
|
+
"multiedit",
|
|
71
|
+
"notebookedit"
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
function extractCandidatePaths(input) {
|
|
75
|
+
if (Array.isArray(input)) return input.flatMap(extractCandidatePaths)
|
|
76
|
+
if (typeof input !== "string") return []
|
|
77
|
+
return input
|
|
78
|
+
.split(",")
|
|
79
|
+
.map((part) => normalizePath(part.trim()))
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getSensitiveFilePatterns(config = {}) {
|
|
84
|
+
const configured = config.tool?.sensitive_file_patterns
|
|
85
|
+
if (!configured) return [...DEFAULT_SENSITIVE_FILE_PATTERNS]
|
|
86
|
+
if (Array.isArray(configured)) return configured.filter((value) => typeof value === "string" && value.trim())
|
|
87
|
+
return [...DEFAULT_SENSITIVE_FILE_PATTERNS]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function isSensitiveEditTool(toolName) {
|
|
91
|
+
return SENSITIVE_EDIT_TOOLS.has(String(toolName || ""))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function isSensitiveEditPath(pathOrPaths, config = {}) {
|
|
95
|
+
const patterns = getSensitiveFilePatterns(config)
|
|
96
|
+
const candidates = extractCandidatePaths(pathOrPaths)
|
|
97
|
+
return candidates.some((candidate) => patterns.some((pattern) => matchGlob(pattern, candidate)))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getSensitiveEditPolicy(toolName, pathOrPaths, config = {}) {
|
|
101
|
+
if (!isSensitiveEditTool(toolName)) return null
|
|
102
|
+
if (!isSensitiveEditPath(pathOrPaths, config)) return null
|
|
103
|
+
return {
|
|
104
|
+
action: "ask",
|
|
105
|
+
source: "sensitive_path",
|
|
106
|
+
reason: "sensitive edit target requires explicit approval"
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/permission/rules.mjs
CHANGED
|
@@ -1,3 +1,84 @@
|
|
|
1
|
+
import { getSensitiveEditPolicy } from "./file-edit-policy.mjs"
|
|
2
|
+
|
|
3
|
+
export const PERMISSION_MODES = ["auto", "manual", "yolo"]
|
|
4
|
+
export const LEGACY_PERMISSION_POLICIES = ["ask", "allow", "deny"]
|
|
5
|
+
|
|
6
|
+
const AUTO_READONLY_TOOLS = new Set([
|
|
7
|
+
"list",
|
|
8
|
+
"read",
|
|
9
|
+
"glob",
|
|
10
|
+
"grep",
|
|
11
|
+
"codesearch",
|
|
12
|
+
"sysinfo",
|
|
13
|
+
"websearch",
|
|
14
|
+
"webfetch",
|
|
15
|
+
"background_output",
|
|
16
|
+
"task_list",
|
|
17
|
+
"task_get",
|
|
18
|
+
"task_output",
|
|
19
|
+
"todowrite",
|
|
20
|
+
"question",
|
|
21
|
+
"enter_plan",
|
|
22
|
+
"exit_plan"
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
const AUTO_REVIEW_ASK_TOOLS = new Set([
|
|
26
|
+
"bash",
|
|
27
|
+
"write",
|
|
28
|
+
"edit",
|
|
29
|
+
"patch",
|
|
30
|
+
"multiedit",
|
|
31
|
+
"notebookedit",
|
|
32
|
+
"task",
|
|
33
|
+
"task_stop",
|
|
34
|
+
"background_cancel",
|
|
35
|
+
"skill"
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
const TRUSTED_BASH_PATTERNS = [
|
|
39
|
+
/^(pwd|ls|cat|head|tail|wc|which|date|whoami|uname)\b/i,
|
|
40
|
+
/^(rg|grep|find)\b/i,
|
|
41
|
+
/^sed\s+-n\b/i,
|
|
42
|
+
/^git\s+(status|log|diff|show|branch|rev-parse)\b/i,
|
|
43
|
+
/^(node|npm|pnpm|yarn)\s+(--version|-v|version|root|list|ls)\b/i
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
function normalizePermissionMode(permission = {}) {
|
|
47
|
+
const mode = String(permission.mode || "").toLowerCase()
|
|
48
|
+
if (PERMISSION_MODES.includes(mode)) return mode
|
|
49
|
+
const legacy = String(permission.default_policy || "").toLowerCase()
|
|
50
|
+
if (legacy === "auto" || legacy === "yolo") return legacy
|
|
51
|
+
return "manual"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function trustedBashCommand(command) {
|
|
55
|
+
const cmd = String(command || "").trim()
|
|
56
|
+
if (!cmd) return false
|
|
57
|
+
if (/[;&|<>`]/.test(cmd)) return false
|
|
58
|
+
return TRUSTED_BASH_PATTERNS.some((pattern) => pattern.test(cmd))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function autoAllowsTool({ tool, command = "" }) {
|
|
62
|
+
if (AUTO_READONLY_TOOLS.has(tool)) return true
|
|
63
|
+
if (tool === "bash") return trustedBashCommand(command)
|
|
64
|
+
if (AUTO_REVIEW_ASK_TOOLS.has(tool)) return false
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function applySensitiveEscalation(decision, { tool, pattern, config, mode }) {
|
|
69
|
+
if (mode === "yolo") return decision
|
|
70
|
+
const sensitivePolicy = getSensitiveEditPolicy(tool, pattern, config)
|
|
71
|
+
if (sensitivePolicy && decision.action === "allow") {
|
|
72
|
+
return {
|
|
73
|
+
action: sensitivePolicy.action,
|
|
74
|
+
source: sensitivePolicy.source,
|
|
75
|
+
rule: decision.rule || null,
|
|
76
|
+
mode
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return decision
|
|
80
|
+
}
|
|
81
|
+
|
|
1
82
|
/**
|
|
2
83
|
* Glob-style pattern matching supporting:
|
|
3
84
|
* * — any chars except /
|
|
@@ -31,9 +112,18 @@ function globToRegex(pattern) {
|
|
|
31
112
|
return new RegExp(`^${src}$`, "i")
|
|
32
113
|
}
|
|
33
114
|
|
|
115
|
+
function normalizePath(p) {
|
|
116
|
+
// Resolve ../ and ./ sequences to prevent traversal bypass
|
|
117
|
+
return p.replace(/\\/g, "/").split("/").reduce((acc, seg) => {
|
|
118
|
+
if (seg === "..") { acc.pop(); return acc }
|
|
119
|
+
if (seg !== "." && seg !== "") acc.push(seg)
|
|
120
|
+
return acc
|
|
121
|
+
}, []).join("/")
|
|
122
|
+
}
|
|
123
|
+
|
|
34
124
|
function matchGlob(value, pattern) {
|
|
35
125
|
if (!pattern || pattern === "*") return true
|
|
36
|
-
const str = String(value || "")
|
|
126
|
+
const str = normalizePath(String(value || ""))
|
|
37
127
|
const negate = pattern.startsWith("!")
|
|
38
128
|
const pat = negate ? pattern.slice(1) : pattern
|
|
39
129
|
const matched = globToRegex(pat).test(str)
|
|
@@ -99,22 +189,41 @@ export function matchRule(rule, input) {
|
|
|
99
189
|
|
|
100
190
|
export function evaluatePermission({ config, tool, mode, pattern = "*", command = "", risk = 0 }) {
|
|
101
191
|
const permission = config.permission || { default_policy: "ask", rules: [] }
|
|
192
|
+
const permissionMode = normalizePermissionMode(permission)
|
|
102
193
|
const rules = Array.isArray(permission.rules) ? permission.rules : []
|
|
103
194
|
for (const rule of rules) {
|
|
104
195
|
if (matchRule(rule, { tool, mode, pattern, command, risk })) {
|
|
105
|
-
|
|
196
|
+
const matchedDecision = {
|
|
106
197
|
action: rule.action,
|
|
107
198
|
source: "rule",
|
|
108
|
-
rule
|
|
199
|
+
rule,
|
|
200
|
+
mode: permissionMode
|
|
109
201
|
}
|
|
202
|
+
return applySensitiveEscalation(matchedDecision, { tool, pattern, config, mode: permissionMode })
|
|
110
203
|
}
|
|
111
204
|
}
|
|
112
|
-
|
|
113
|
-
|
|
205
|
+
|
|
206
|
+
if (permissionMode === "yolo") {
|
|
207
|
+
return { action: "allow", source: "mode:yolo", rule: null, mode: permissionMode }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (permissionMode === "auto") {
|
|
211
|
+
const action = autoAllowsTool({ tool, command, risk }) ? "allow" : "ask"
|
|
212
|
+
const decision = { action, source: "auto_review", rule: null, mode: permissionMode }
|
|
213
|
+
return applySensitiveEscalation(decision, { tool, pattern, config, mode: permissionMode })
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const defaultPolicy = LEGACY_PERMISSION_POLICIES.includes(permission.default_policy)
|
|
217
|
+
? permission.default_policy
|
|
218
|
+
: "ask"
|
|
219
|
+
const fallbackDecision = {
|
|
220
|
+
action: defaultPolicy,
|
|
114
221
|
source: "default",
|
|
115
|
-
rule: null
|
|
222
|
+
rule: null,
|
|
223
|
+
mode: permissionMode
|
|
116
224
|
}
|
|
225
|
+
return applySensitiveEscalation(fallbackDecision, { tool, pattern, config, mode: permissionMode })
|
|
117
226
|
}
|
|
118
227
|
|
|
119
228
|
// Exported for testing
|
|
120
|
-
export { matchGlob, matchPatterns, matchCommandPrefix }
|
|
229
|
+
export { matchGlob, matchPatterns, matchCommandPrefix, normalizePermissionMode, trustedBashCommand, autoAllowsTool }
|
|
@@ -18,7 +18,8 @@ export default {
|
|
|
18
18
|
name: "post-edit-format",
|
|
19
19
|
tool: {
|
|
20
20
|
async after(payload) {
|
|
21
|
-
const
|
|
21
|
+
const toolName = String(payload.toolName || payload.tool || "")
|
|
22
|
+
const { args, cwd } = payload
|
|
22
23
|
if (!["edit", "write", "multiedit"].includes(toolName)) return payload
|
|
23
24
|
|
|
24
25
|
// Collect affected files
|
|
@@ -1,61 +1,125 @@
|
|
|
1
|
-
// Post-edit
|
|
2
|
-
//
|
|
1
|
+
// Post-edit diagnostics + observability hook
|
|
2
|
+
// Captures baseline diagnostics before mutation tools and appends a concise
|
|
3
|
+
// post-edit diagnostics delta plus mutation summary after mutation tools run.
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import {
|
|
6
|
+
buildEditDiagnosticsReport,
|
|
7
|
+
buildMutationObservability,
|
|
8
|
+
collectDiagnosticsSnapshot,
|
|
9
|
+
extractTouchedFiles,
|
|
10
|
+
isDiagnosticsEligibleFile,
|
|
11
|
+
isMutationTool
|
|
12
|
+
} from "../../observability/edit-diagnostics.mjs"
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
function normalizeToolName(payload = {}) {
|
|
15
|
+
return String(payload.toolName || payload.tool || "").trim()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isCompletedResult(result) {
|
|
19
|
+
if (!result || typeof result !== "object") return true
|
|
20
|
+
return !result.status || result.status === "completed"
|
|
21
|
+
}
|
|
10
22
|
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
function appendFeedback(result, reportText) {
|
|
24
|
+
if (!reportText) return result
|
|
25
|
+
if (typeof result === "string") return `${result}\n${reportText}`.trim()
|
|
26
|
+
if (result && typeof result === "object") {
|
|
27
|
+
return {
|
|
28
|
+
...result,
|
|
29
|
+
output: `${String(result.output || "")}\n${reportText}`.trim()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildReportText({ observability, diagnostics }) {
|
|
36
|
+
const lines = []
|
|
37
|
+
if (observability?.changes?.length) {
|
|
38
|
+
lines.push("Mutation summary:")
|
|
39
|
+
lines.push(`- ${observability.summary}`)
|
|
40
|
+
}
|
|
41
|
+
if (diagnostics?.summary?.text) {
|
|
42
|
+
lines.push("Diagnostics:")
|
|
43
|
+
lines.push(`- ${diagnostics.summary.text}`)
|
|
44
|
+
for (const issue of (diagnostics.delta?.added || []).slice(0, 2)) {
|
|
45
|
+
lines.push(`- introduced ${issue.file || "unknown"} ${issue.code || ""} ${issue.message || ""}`.trim())
|
|
46
|
+
}
|
|
47
|
+
for (const issue of (diagnostics.delta?.resolved || []).slice(0, 2)) {
|
|
48
|
+
lines.push(`- resolved ${issue.file || "unknown"} ${issue.code || ""} ${issue.message || ""}`.trim())
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return lines.join("\n")
|
|
13
52
|
}
|
|
14
53
|
|
|
15
54
|
export default {
|
|
16
55
|
name: "post-edit-typecheck",
|
|
17
56
|
tool: {
|
|
18
|
-
async
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const files =
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
57
|
+
async before(payload) {
|
|
58
|
+
const toolName = normalizeToolName(payload)
|
|
59
|
+
if (!isMutationTool(toolName)) return payload
|
|
60
|
+
|
|
61
|
+
const cwd = payload.cwd || process.cwd()
|
|
62
|
+
const files = extractTouchedFiles({ args: payload.args }).filter(isDiagnosticsEligibleFile)
|
|
63
|
+
if (files.length === 0) return payload
|
|
64
|
+
|
|
65
|
+
const baseline = await collectDiagnosticsSnapshot({ cwd, files }).catch(() => null)
|
|
66
|
+
return {
|
|
67
|
+
...payload,
|
|
68
|
+
_editObservability: {
|
|
69
|
+
files,
|
|
70
|
+
baseline
|
|
28
71
|
}
|
|
29
72
|
}
|
|
73
|
+
},
|
|
30
74
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
75
|
+
async after(payload) {
|
|
76
|
+
const toolName = normalizeToolName(payload)
|
|
77
|
+
if (!isMutationTool(toolName)) return payload
|
|
78
|
+
if (!isCompletedResult(payload.result)) return payload
|
|
79
|
+
|
|
80
|
+
const cwd = payload.cwd || process.cwd()
|
|
81
|
+
const metadata = payload.result && typeof payload.result === "object" && payload.result.metadata && typeof payload.result.metadata === "object"
|
|
82
|
+
? { ...payload.result.metadata }
|
|
83
|
+
: {}
|
|
84
|
+
const files = (payload._editObservability?.files || extractTouchedFiles({ args: payload.args, metadata }))
|
|
85
|
+
.filter(isDiagnosticsEligibleFile)
|
|
34
86
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!(await fileExists(tsconfigPath))) return payload
|
|
87
|
+
const observability = buildMutationObservability(metadata)
|
|
88
|
+
let diagnostics = null
|
|
38
89
|
|
|
39
|
-
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
|
|
90
|
+
if (files.length > 0) {
|
|
91
|
+
const current = await collectDiagnosticsSnapshot({ cwd, files }).catch(() => null)
|
|
92
|
+
diagnostics = buildEditDiagnosticsReport({
|
|
93
|
+
cwd,
|
|
94
|
+
files,
|
|
95
|
+
baseline: payload._editObservability?.baseline || {},
|
|
96
|
+
current: current || {},
|
|
97
|
+
reason: current ? "" : "snapshot_failed"
|
|
43
98
|
})
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!observability.changes.length && !diagnostics) {
|
|
102
|
+
return payload
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const reportText = buildReportText({ observability, diagnostics })
|
|
106
|
+
let nextResult = appendFeedback(payload.result, reportText)
|
|
107
|
+
|
|
108
|
+
if (nextResult && typeof nextResult === "object") {
|
|
109
|
+
nextResult = {
|
|
110
|
+
...nextResult,
|
|
111
|
+
metadata: {
|
|
112
|
+
...metadata,
|
|
113
|
+
observability,
|
|
114
|
+
...(diagnostics ? { diagnostics } : {})
|
|
54
115
|
}
|
|
55
116
|
}
|
|
56
117
|
}
|
|
57
118
|
|
|
58
|
-
return
|
|
119
|
+
return {
|
|
120
|
+
...payload,
|
|
121
|
+
result: nextResult
|
|
122
|
+
}
|
|
59
123
|
}
|
|
60
124
|
}
|
|
61
125
|
}
|
package/src/plugin/hook-bus.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
2
|
import { access, readdir } from "node:fs/promises"
|
|
3
3
|
import { pathToFileURL, fileURLToPath } from "node:url"
|
|
4
|
+
import { userRootDir } from "../storage/paths.mjs"
|
|
4
5
|
|
|
5
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
6
7
|
|
|
@@ -17,7 +18,8 @@ const HOOK_EVENTS = [
|
|
|
17
18
|
const state = {
|
|
18
19
|
loaded: false,
|
|
19
20
|
hooks: [],
|
|
20
|
-
errors: []
|
|
21
|
+
errors: [],
|
|
22
|
+
warnedPluginAlias: false
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
function normalizeHook(mod, source) {
|
|
@@ -63,11 +65,23 @@ export async function initHookBus(cwd = process.cwd()) {
|
|
|
63
65
|
if (state.loaded) return state
|
|
64
66
|
// Built-in hooks ship with kkcode (lowest priority — user hooks can override)
|
|
65
67
|
const builtinHooks = path.join(__dirname, "builtin-hooks")
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
+
const userHooks = path.join(userRootDir(), "hooks")
|
|
69
|
+
const projectPluginHooks = path.join(cwd, ".kkcode", "plugins")
|
|
68
70
|
const projectHooks = path.join(cwd, ".kkcode", "hooks")
|
|
69
|
-
// Load order: builtin → user → project
|
|
70
|
-
|
|
71
|
+
// Load order: builtin → user → project plugin alias → project hooks
|
|
72
|
+
// `.kkcode/plugins` remains a compatibility alias for hook scripts while
|
|
73
|
+
// `.kkcode/hooks` is the explicit project hook path.
|
|
74
|
+
const pluginAliasFiles = await discover(projectPluginHooks)
|
|
75
|
+
if (pluginAliasFiles.length && !state.warnedPluginAlias) {
|
|
76
|
+
state.errors.push("deprecated hook path: .kkcode/plugins is a compatibility alias for loose hook scripts; prefer .kkcode/hooks or a plugin.json package boundary")
|
|
77
|
+
state.warnedPluginAlias = true
|
|
78
|
+
}
|
|
79
|
+
const files = [
|
|
80
|
+
...(await discover(builtinHooks)),
|
|
81
|
+
...(await discover(userHooks)),
|
|
82
|
+
...pluginAliasFiles,
|
|
83
|
+
...(await discover(projectHooks))
|
|
84
|
+
]
|
|
71
85
|
for (const file of files) {
|
|
72
86
|
const loaded = await loadModule(file)
|
|
73
87
|
if (loaded.error) {
|