@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,135 @@
1
+ import { paint } from "./color.mjs"
2
+
3
+ const COLORS = {
4
+ code: "cyan",
5
+ codeBlock: "#a9b7c6",
6
+ codeFence: "#555555",
7
+ heading: "white",
8
+ quote: "#8da3b9",
9
+ listMarker: "#8a8a8a"
10
+ }
11
+
12
+ function renderLine(line) {
13
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)/)
14
+ if (headingMatch) {
15
+ const level = headingMatch[1].length
16
+ const content = headingMatch[2]
17
+ const indent = level > 1 ? " ".repeat(level - 1) : ""
18
+ return `${indent}${paint(renderInline(content), COLORS.heading, { bold: level <= 2 })}`
19
+ }
20
+
21
+ if (line.trimStart().startsWith("> ")) {
22
+ const content = line.replace(/^\s*>\s?/, "")
23
+ return paint(`\u2502 ${renderInline(content)}`, COLORS.quote)
24
+ }
25
+
26
+ const ulMatch = line.match(/^(\s*)([-*+])\s+(.*)/)
27
+ if (ulMatch) {
28
+ return `${ulMatch[1]}${paint("\u2022", COLORS.listMarker)} ${renderInline(ulMatch[3])}`
29
+ }
30
+
31
+ const olMatch = line.match(/^(\s*)(\d+)\.\s+(.*)/)
32
+ if (olMatch) {
33
+ return `${olMatch[1]}${paint(`${olMatch[2]}.`, COLORS.listMarker)} ${renderInline(olMatch[3])}`
34
+ }
35
+
36
+ return renderInline(line)
37
+ }
38
+
39
+ function renderInline(text) {
40
+ return text
41
+ .replace(/`([^`]+)`/g, (_, code) => paint(code, COLORS.code))
42
+ .replace(/\*\*([^*]+)\*\*/g, (_, b) => paint(b, null, { bold: true }))
43
+ .replace(/__([^_]+)__/g, (_, b) => paint(b, null, { bold: true }))
44
+ .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, i) => paint(i, null, { dim: true }))
45
+ .replace(/(?<!_)_([^_]+)_(?!_)/g, (_, i) => paint(i, null, { dim: true }))
46
+ }
47
+
48
+ export function renderMarkdown(text) {
49
+ if (!text) return ""
50
+ const lines = text.split("\n")
51
+ const out = []
52
+ let inCodeBlock = false
53
+ let codeLang = ""
54
+
55
+ for (const line of lines) {
56
+ if (line.trimStart().startsWith("```")) {
57
+ if (!inCodeBlock) {
58
+ codeLang = line.trimStart().slice(3).trim()
59
+ const label = codeLang ? ` ${codeLang} ` : ""
60
+ out.push(paint(`\u2500\u2500\u2500${label}${"".padEnd(Math.max(0, 40 - label.length), "\u2500")}`, COLORS.codeFence))
61
+ inCodeBlock = true
62
+ } else {
63
+ out.push(paint("\u2500".repeat(43), COLORS.codeFence))
64
+ inCodeBlock = false
65
+ codeLang = ""
66
+ }
67
+ continue
68
+ }
69
+
70
+ if (inCodeBlock) {
71
+ out.push(paint(` ${line}`, COLORS.codeBlock))
72
+ continue
73
+ }
74
+
75
+ out.push(renderLine(line))
76
+ }
77
+ return out.join("\n")
78
+ }
79
+
80
+ export function createStreamRenderer() {
81
+ let buffer = ""
82
+ let inCodeBlock = false
83
+
84
+ function push(chunk) {
85
+ buffer += chunk
86
+ const output = []
87
+
88
+ while (true) {
89
+ const idx = buffer.indexOf("\n")
90
+ if (idx === -1) break
91
+
92
+ const line = buffer.slice(0, idx)
93
+ buffer = buffer.slice(idx + 1)
94
+
95
+ if (line.trimStart().startsWith("```")) {
96
+ if (!inCodeBlock) {
97
+ const lang = line.trimStart().slice(3).trim()
98
+ const label = lang ? ` ${lang} ` : ""
99
+ output.push(paint(`\u2500\u2500\u2500${label}${"".padEnd(Math.max(0, 40 - label.length), "\u2500")}`, COLORS.codeFence) + "\n")
100
+ inCodeBlock = true
101
+ } else {
102
+ output.push(paint("\u2500".repeat(43), COLORS.codeFence) + "\n")
103
+ inCodeBlock = false
104
+ }
105
+ continue
106
+ }
107
+
108
+ if (inCodeBlock) {
109
+ output.push(paint(` ${line}`, COLORS.codeBlock) + "\n")
110
+ } else {
111
+ output.push(renderLine(line) + "\n")
112
+ }
113
+ }
114
+
115
+ if (!inCodeBlock && buffer.length > 0 && !buffer.startsWith("```")) {
116
+ const partial = renderInline(buffer)
117
+ buffer = ""
118
+ output.push(partial)
119
+ }
120
+
121
+ return output.join("")
122
+ }
123
+
124
+ function flush() {
125
+ if (!buffer) return ""
126
+ const remaining = inCodeBlock
127
+ ? paint(` ${buffer}`, COLORS.codeBlock)
128
+ : renderLine(buffer)
129
+ buffer = ""
130
+ inCodeBlock = false
131
+ return remaining
132
+ }
133
+
134
+ return { push, flush }
135
+ }
@@ -0,0 +1,45 @@
1
+ const REQUIRED_GROUPS = ["base", "semantic", "modes", "components"]
2
+ const MODE_KEYS = ["ask", "plan", "agent", "longagent"]
3
+ const HEX_RE = /^#([A-Fa-f0-9]{6})$/
4
+
5
+ function validateColor(value, path, errors) {
6
+ if (typeof value !== "string" || !HEX_RE.test(value)) {
7
+ errors.push(`${path} must be a hex color like #00ff00`)
8
+ }
9
+ }
10
+
11
+ export function validateTheme(theme) {
12
+ const errors = []
13
+ if (!theme || typeof theme !== "object" || Array.isArray(theme)) {
14
+ return { valid: false, errors: ["theme must be an object"] }
15
+ }
16
+ if (typeof theme.name !== "string" || theme.name.trim().length === 0) {
17
+ errors.push("name must be a non-empty string")
18
+ }
19
+ for (const group of REQUIRED_GROUPS) {
20
+ if (!theme[group] || typeof theme[group] !== "object" || Array.isArray(theme[group])) {
21
+ errors.push(`${group} must be an object`)
22
+ }
23
+ }
24
+ if (theme.base) {
25
+ for (const key of ["bg", "fg", "muted", "border", "accent"]) {
26
+ validateColor(theme.base[key], `base.${key}`, errors)
27
+ }
28
+ }
29
+ if (theme.semantic) {
30
+ for (const key of ["info", "warn", "error", "success"]) {
31
+ validateColor(theme.semantic[key], `semantic.${key}`, errors)
32
+ }
33
+ }
34
+ if (theme.components) {
35
+ for (const key of ["panel", "header", "footer", "diff_add", "diff_del"]) {
36
+ validateColor(theme.components[key], `components.${key}`, errors)
37
+ }
38
+ }
39
+ if (theme.modes) {
40
+ for (const key of MODE_KEYS) {
41
+ validateColor(theme.modes[key], `modes.${key}`, errors)
42
+ }
43
+ }
44
+ return { valid: errors.length === 0, errors }
45
+ }
@@ -0,0 +1,158 @@
1
+ import { paint } from "./color.mjs"
2
+
3
+ function formatNumber(value) {
4
+ return Intl.NumberFormat("en-US").format(Math.round(value))
5
+ }
6
+
7
+ function formatCost(amount) {
8
+ if (amount === null || amount === undefined) return "unknown"
9
+ return `$${amount.toFixed(4)}`
10
+ }
11
+
12
+ function permissionColor(permission, theme) {
13
+ switch (permission) {
14
+ case "allow": return theme.semantic.success || theme.semantic.info
15
+ case "deny": return theme.semantic.error || theme.semantic.warn
16
+ case "ask":
17
+ default:
18
+ return theme.semantic.info
19
+ }
20
+ }
21
+
22
+ function contrastText(hex, dark = "#111111", light = "#f7f7f7") {
23
+ if (!/^#([A-Fa-f0-9]{6})$/.test(String(hex || ""))) return light
24
+ const raw = hex.replace("#", "")
25
+ const r = parseInt(raw.slice(0, 2), 16)
26
+ const g = parseInt(raw.slice(2, 4), 16)
27
+ const b = parseInt(raw.slice(4, 6), 16)
28
+ const y = 0.2126 * r + 0.7152 * g + 0.0722 * b
29
+ return y > 150 ? dark : light
30
+ }
31
+
32
+ function badge(text, fg, bg, options = {}) {
33
+ return paint(` ${text} `, fg, { bg, bold: options.bold !== false })
34
+ }
35
+
36
+ function clipModel(model, maxLen) {
37
+ const value = String(model || "")
38
+ if (value.length <= maxLen) return value
39
+ if (maxLen < 10) return value.slice(0, maxLen)
40
+ return `${value.slice(0, Math.max(4, maxLen - 4))}...`
41
+ }
42
+
43
+ export function renderStatusBar({
44
+ mode,
45
+ model,
46
+ permission,
47
+ tokenMeter,
48
+ aggregation = ["turn", "session", "global"],
49
+ cost,
50
+ savings = 0,
51
+ showCost = true,
52
+ showTokenMeter = true,
53
+ contextMeter = null,
54
+ theme,
55
+ layout = "compact",
56
+ longagentState = null,
57
+ memoryLoaded = false
58
+ }) {
59
+ const width = Number(process.stdout.columns || 120)
60
+ const dense = width < 110
61
+ const tight = width < 86
62
+ const modelLabel = clipModel(model, tight ? 18 : dense ? 28 : 44)
63
+
64
+ const segments = []
65
+ const modeBg = theme.modes[mode] || theme.base.accent
66
+ segments.push(badge(mode.toUpperCase(), contrastText(modeBg), modeBg))
67
+ segments.push(badge(`MODEL ${modelLabel}`, theme.base.fg, theme.components.panel || theme.base.border, { bold: false }))
68
+
69
+ if (showTokenMeter && tokenMeter) {
70
+ const t = tokenMeter.turn
71
+ const s = tokenMeter.session
72
+ const g = tokenMeter.global
73
+ const tokenSegments = []
74
+ if (aggregation.includes("turn")) tokenSegments.push(`T:${formatNumber(t.input + t.output)}`)
75
+ if (!tight && aggregation.includes("session")) tokenSegments.push(`S:${formatNumber(s.input + s.output)}`)
76
+ if (!dense && aggregation.includes("global")) tokenSegments.push(`G:${formatNumber(g.input + g.output)}`)
77
+ const tokenText = `TOKENS ${tokenSegments.join(" ")}${tokenMeter.estimated ? " ~" : ""}`
78
+ segments.push(
79
+ badge(tokenText, theme.base.fg, "#2d3748", { bold: false })
80
+ )
81
+ }
82
+ if (showCost) {
83
+ const savingsStr = savings > 0 ? ` ↓${formatCost(savings)}` : ""
84
+ segments.push(badge(`COST ${formatCost(cost)}${savingsStr}`, contrastText(theme.semantic.warn), theme.semantic.warn, { bold: false }))
85
+ }
86
+ if (contextMeter && Number.isFinite(contextMeter.percent)) {
87
+ const pct = Math.max(0, Math.min(100, Math.round(contextMeter.percent)))
88
+ const ctxBg = pct >= 85
89
+ ? theme.semantic.error
90
+ : pct >= 70
91
+ ? theme.semantic.warn
92
+ : theme.semantic.info
93
+ let suffix = ""
94
+ if (contextMeter.cacheRead > 0 || contextMeter.cacheWrite > 0) {
95
+ const total = (contextMeter.cacheRead || 0) + (contextMeter.cacheWrite || 0) + (contextMeter.inputUncached || 0)
96
+ const hitPct = total > 0 ? Math.round((contextMeter.cacheRead || 0) / total * 100) : 0
97
+ suffix = ` Cache:${hitPct}%`
98
+ }
99
+ const text = tight ? `CTX ${pct}%` : `CONTEXT ${pct}%${suffix}`
100
+ segments.push(badge(text, contrastText(ctxBg), ctxBg, { bold: false }))
101
+ }
102
+ if (memoryLoaded && !tight) {
103
+ segments.push(badge("MEM", contrastText(theme.semantic.info), theme.semantic.info, { bold: false }))
104
+ }
105
+ const permBg = permissionColor(permission, theme)
106
+ segments.push(badge(`PERMISSION ${permission.toUpperCase()}`, contrastText(permBg), permBg, { bold: false }))
107
+ if (longagentState && mode === "longagent") {
108
+ const parts = []
109
+ if (longagentState.currentStageId) {
110
+ parts.push(`STG:${longagentState.currentStageId}`)
111
+ } else if (Number.isFinite(longagentState.stageIndex) && Number.isFinite(longagentState.stageCount) && longagentState.stageCount > 0) {
112
+ parts.push(`STG:${longagentState.stageIndex + 1}/${longagentState.stageCount}`)
113
+ }
114
+ if (longagentState.stageProgress?.total) {
115
+ parts.push(`TSK:${longagentState.stageProgress.done || 0}/${longagentState.stageProgress.total}`)
116
+ }
117
+ if (Number.isFinite(longagentState.remainingFilesCount)) {
118
+ parts.push(`REM:${longagentState.remainingFilesCount}`)
119
+ }
120
+ if (longagentState.phase) {
121
+ parts.push(`P:${longagentState.phase}`)
122
+ }
123
+ if (longagentState.currentGate) {
124
+ parts.push(`G:${longagentState.currentGate}`)
125
+ }
126
+ if (longagentState.iterations !== undefined) {
127
+ const iter = longagentState.maxIterations
128
+ ? `${longagentState.iterations}/${longagentState.maxIterations}`
129
+ : String(longagentState.iterations)
130
+ parts.push(`I:${iter}`)
131
+ }
132
+ if (!tight && longagentState.progress?.percentage !== null && longagentState.progress?.percentage !== undefined) {
133
+ const pct = longagentState.progress.percentage
134
+ const barW = dense ? 8 : 14
135
+ const filled = Math.round(barW * pct / 100)
136
+ parts.push(`${"█".repeat(filled)}${"░".repeat(barW - filled)} ${pct}%`)
137
+ }
138
+ if (!dense && longagentState.elapsed !== undefined) {
139
+ const m = Math.floor(longagentState.elapsed / 60)
140
+ const s = longagentState.elapsed % 60
141
+ parts.push(`${m}m${s}s`)
142
+ }
143
+ if (!tight && Array.isArray(longagentState.lastGateFailures) && longagentState.lastGateFailures.length) {
144
+ parts.push(`Fail`)
145
+ }
146
+ if (!tight && typeof longagentState.recoveryCount === "number" && longagentState.recoveryCount > 0) {
147
+ parts.push(`R:${longagentState.recoveryCount}`)
148
+ }
149
+ if (parts.length) {
150
+ segments.push(badge(`LONG ${parts.join(" ")}`, contrastText(theme.semantic.success), theme.semantic.success, { bold: false }))
151
+ }
152
+ }
153
+
154
+ if (layout === "comfortable") {
155
+ return segments.join(" ")
156
+ }
157
+ return segments.join(" ")
158
+ }
@@ -0,0 +1,38 @@
1
+ import { appendAuditEntry } from "../storage/audit-store.mjs"
2
+
3
+ export async function withAudit({ sessionId, turnId, toolName, args, run }) {
4
+ const startedAt = Date.now()
5
+ await appendAuditEntry({
6
+ type: "tool.start",
7
+ sessionId,
8
+ turnId,
9
+ tool: toolName,
10
+ args
11
+ })
12
+ try {
13
+ const result = await run()
14
+ await appendAuditEntry({
15
+ type: "tool.finish",
16
+ sessionId,
17
+ turnId,
18
+ tool: toolName,
19
+ durationMs: Date.now() - startedAt,
20
+ ok: result?.status !== "error" && result?.status !== "cancelled",
21
+ status: result?.status,
22
+ output: result?.output?.slice(0, 2000) || ""
23
+ })
24
+ return result
25
+ } catch (error) {
26
+ await appendAuditEntry({
27
+ type: "tool.error",
28
+ sessionId,
29
+ turnId,
30
+ tool: toolName,
31
+ durationMs: Date.now() - startedAt,
32
+ ok: false,
33
+ status: "error",
34
+ error: error.message
35
+ })
36
+ throw error
37
+ }
38
+ }
@@ -0,0 +1,126 @@
1
+ import path from "node:path"
2
+ import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises"
3
+
4
+ function tmpPath(target) {
5
+ return `${target}.kkcode.tmp`
6
+ }
7
+
8
+ function backupPath(target) {
9
+ return `${target}.kkcode.bak`
10
+ }
11
+
12
+ /**
13
+ * Count added/removed lines between two text snippets using LCS.
14
+ * For snippets under 500 lines, uses O(m*n) DP. For larger texts, falls back to simple line-count diff.
15
+ */
16
+ export function diffLineCount(oldText, newText) {
17
+ const oldLines = String(oldText || "").split(/\r?\n/)
18
+ const newLines = String(newText || "").split(/\r?\n/)
19
+ const m = oldLines.length
20
+ const n = newLines.length
21
+
22
+ // Fast path: identical
23
+ if (oldText === newText) return { added: 0, removed: 0 }
24
+
25
+ // For large texts, fall back to simple counting to avoid O(m*n) blowup
26
+ if (m > 500 || n > 500) {
27
+ // Build a set of old lines with counts
28
+ const oldCounts = new Map()
29
+ for (const line of oldLines) oldCounts.set(line, (oldCounts.get(line) || 0) + 1)
30
+ const newCounts = new Map()
31
+ for (const line of newLines) newCounts.set(line, (newCounts.get(line) || 0) + 1)
32
+ let common = 0
33
+ for (const [line, count] of oldCounts) {
34
+ common += Math.min(count, newCounts.get(line) || 0)
35
+ }
36
+ return { added: n - common, removed: m - common }
37
+ }
38
+
39
+ // LCS via DP
40
+ const dp = Array.from({ length: m + 1 }, () => new Uint16Array(n + 1))
41
+ for (let i = 1; i <= m; i++) {
42
+ for (let j = 1; j <= n; j++) {
43
+ if (oldLines[i - 1] === newLines[j - 1]) {
44
+ dp[i][j] = dp[i - 1][j - 1] + 1
45
+ } else {
46
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
47
+ }
48
+ }
49
+ }
50
+ const common = dp[m][n]
51
+ return { added: n - common, removed: m - common }
52
+ }
53
+
54
+ export async function atomicWriteFile(target, content) {
55
+ const dir = path.dirname(target)
56
+ await mkdir(dir, { recursive: true })
57
+ const tmp = tmpPath(target)
58
+ const bak = backupPath(target)
59
+ let hadOriginal = false
60
+ try {
61
+ const existing = await readFile(target, "utf8")
62
+ hadOriginal = true
63
+ await writeFile(bak, existing, "utf8")
64
+ } catch {
65
+ hadOriginal = false
66
+ }
67
+
68
+ try {
69
+ await writeFile(tmp, content, "utf8")
70
+ await rename(tmp, target)
71
+ if (hadOriginal) {
72
+ await unlink(bak).catch(() => {})
73
+ }
74
+ } catch (error) {
75
+ if (hadOriginal) {
76
+ const bakContent = await readFile(bak, "utf8").catch(() => null)
77
+ if (bakContent !== null) {
78
+ await writeFile(target, bakContent, "utf8").catch(() => {})
79
+ }
80
+ await unlink(bak).catch(() => {})
81
+ }
82
+ await unlink(tmp).catch(() => {})
83
+ throw error
84
+ }
85
+ }
86
+
87
+ export async function replaceInFileTransactional(target, before, after) {
88
+ const absolute = path.resolve(target)
89
+ const content = await readFile(absolute, "utf8")
90
+ const matches = content.split(before).length - 1
91
+ if (matches <= 0) {
92
+ return { ok: false, output: "no match", matches: 0, addedLines: 0, removedLines: 0 }
93
+ }
94
+ if (matches > 1) {
95
+ return { ok: false, output: `ambiguous: found ${matches} occurrences, expected exactly 1. Provide more surrounding context to match uniquely.`, matches, addedLines: 0, removedLines: 0 }
96
+ }
97
+ const next = content.replace(before, after)
98
+ await atomicWriteFile(absolute, next)
99
+ const diff = diffLineCount(before, after)
100
+ return {
101
+ ok: true,
102
+ output: `replaced 1 occurrence`,
103
+ matches: 1,
104
+ addedLines: diff.added,
105
+ removedLines: diff.removed
106
+ }
107
+ }
108
+
109
+ export async function replaceAllInFileTransactional(target, before, after) {
110
+ const absolute = path.resolve(target)
111
+ const content = await readFile(absolute, "utf8")
112
+ const matches = content.split(before).length - 1
113
+ if (matches <= 0) {
114
+ return { ok: false, output: "no match", matches: 0, addedLines: 0, removedLines: 0 }
115
+ }
116
+ const next = content.replaceAll(before, after)
117
+ await atomicWriteFile(absolute, next)
118
+ const diff = diffLineCount(content, next)
119
+ return {
120
+ ok: true,
121
+ output: `replaced ${matches} occurrence(s)`,
122
+ matches,
123
+ addedLines: diff.added,
124
+ removedLines: diff.removed
125
+ }
126
+ }
@@ -0,0 +1,109 @@
1
+ import { makeToolResult } from "../core/types.mjs"
2
+ import { EventBus } from "../core/events.mjs"
3
+ import { EVENT_TYPES } from "../core/constants.mjs"
4
+ import { withAudit } from "./audit-wrapper.mjs"
5
+
6
+ export async function executeTool({ tool, args, sessionId, turnId, context, signal = null }) {
7
+ return withAudit({
8
+ sessionId,
9
+ turnId,
10
+ toolName: tool.name,
11
+ args,
12
+ run: async () => {
13
+ const startedAt = Date.now()
14
+ await EventBus.emit({
15
+ type: EVENT_TYPES.TOOL_START,
16
+ sessionId,
17
+ turnId,
18
+ payload: {
19
+ tool: tool.name,
20
+ args
21
+ }
22
+ })
23
+
24
+ try {
25
+ if (signal?.aborted) {
26
+ const cancelled = makeToolResult({
27
+ name: tool.name,
28
+ status: "cancelled",
29
+ output: "tool cancelled before execution",
30
+ durationMs: Date.now() - startedAt
31
+ })
32
+ await EventBus.emit({
33
+ type: EVENT_TYPES.TOOL_ERROR,
34
+ sessionId,
35
+ turnId,
36
+ payload: {
37
+ tool: tool.name,
38
+ status: cancelled.status,
39
+ output: cancelled.output,
40
+ args,
41
+ durationMs: cancelled.durationMs
42
+ }
43
+ })
44
+ return cancelled
45
+ }
46
+
47
+ const raw = await tool.execute(args || {}, context)
48
+ let output = ""
49
+ let metadata = {}
50
+ if (typeof raw === "string") {
51
+ output = raw
52
+ } else if (raw && typeof raw === "object") {
53
+ if (typeof raw.output === "string") {
54
+ output = raw.output
55
+ } else {
56
+ output = JSON.stringify(raw, null, 2)
57
+ }
58
+ if (raw.metadata && typeof raw.metadata === "object") {
59
+ metadata = raw.metadata
60
+ }
61
+ } else {
62
+ output = String(raw ?? "")
63
+ }
64
+ const result = makeToolResult({
65
+ name: tool.name,
66
+ status: "completed",
67
+ output,
68
+ durationMs: Date.now() - startedAt,
69
+ metadata
70
+ })
71
+ await EventBus.emit({
72
+ type: EVENT_TYPES.TOOL_FINISH,
73
+ sessionId,
74
+ turnId,
75
+ payload: {
76
+ tool: tool.name,
77
+ status: result.status,
78
+ args,
79
+ output: String(output || "").slice(0, 500),
80
+ durationMs: result.durationMs
81
+ }
82
+ })
83
+ return result
84
+ } catch (error) {
85
+ const errorMessage = error?.message || String(error)
86
+ const result = makeToolResult({
87
+ name: tool.name,
88
+ status: "error",
89
+ output: errorMessage,
90
+ error: errorMessage,
91
+ durationMs: Date.now() - startedAt
92
+ })
93
+ await EventBus.emit({
94
+ type: EVENT_TYPES.TOOL_ERROR,
95
+ sessionId,
96
+ turnId,
97
+ payload: {
98
+ tool: tool.name,
99
+ status: result.status,
100
+ error: result.error,
101
+ args,
102
+ durationMs: result.durationMs
103
+ }
104
+ })
105
+ return result
106
+ }
107
+ }
108
+ })
109
+ }
@@ -0,0 +1,85 @@
1
+ import path from "node:path"
2
+ import { createHash } from "node:crypto"
3
+ import { mkdir, open, unlink, writeFile } from "node:fs/promises"
4
+ import { userRootDir } from "../storage/paths.mjs"
5
+
6
+ const LOCK_POLL_MS = 80
7
+
8
+ function sleep(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms))
10
+ }
11
+
12
+ function lockDir() {
13
+ return path.join(userRootDir(), "locks")
14
+ }
15
+
16
+ function lockFilePath(targetPath) {
17
+ const absolute = path.resolve(targetPath)
18
+ const hash = createHash("sha1").update(absolute).digest("hex")
19
+ return path.join(lockDir(), `${hash}.lock`)
20
+ }
21
+
22
+ async function ensureLockDir() {
23
+ await mkdir(lockDir(), { recursive: true })
24
+ }
25
+
26
+ export async function acquireFileLock({
27
+ targetPath,
28
+ owner = "unknown",
29
+ waitTimeoutMs = 120000
30
+ }) {
31
+ await ensureLockDir()
32
+ const lockFile = lockFilePath(targetPath)
33
+ const started = Date.now()
34
+ while (Date.now() - started <= waitTimeoutMs) {
35
+ try {
36
+ const fd = await open(lockFile, "wx")
37
+ const metadata = {
38
+ owner,
39
+ pid: process.pid,
40
+ targetPath: path.resolve(targetPath),
41
+ acquiredAt: Date.now()
42
+ }
43
+ await fd.writeFile(JSON.stringify(metadata, null, 2), "utf8")
44
+ await fd.close()
45
+ return {
46
+ lockFile,
47
+ owner,
48
+ acquiredAt: metadata.acquiredAt
49
+ }
50
+ } catch (error) {
51
+ if (error?.code !== "EEXIST") throw error
52
+ await sleep(LOCK_POLL_MS)
53
+ }
54
+ }
55
+ const err = new Error(`file lock timeout: ${targetPath}`)
56
+ err.code = "LOCK_TIMEOUT"
57
+ throw err
58
+ }
59
+
60
+ export async function releaseFileLock(lockHandle) {
61
+ if (!lockHandle?.lockFile) return
62
+ await unlink(lockHandle.lockFile).catch(() => {})
63
+ }
64
+
65
+ export async function withFileLock({
66
+ targetPath,
67
+ owner = "unknown",
68
+ waitTimeoutMs = 120000,
69
+ run
70
+ }) {
71
+ const lock = await acquireFileLock({ targetPath, owner, waitTimeoutMs })
72
+ try {
73
+ return await run()
74
+ } finally {
75
+ await releaseFileLock(lock)
76
+ }
77
+ }
78
+
79
+ export async function writeLockDebug(targetPath, owner = "unknown") {
80
+ await ensureLockDir()
81
+ const infoFile = `${lockFilePath(targetPath)}.meta`
82
+ await writeFile(infoFile, JSON.stringify({ owner, at: Date.now() }, null, 2), "utf8")
83
+ return infoFile
84
+ }
85
+