@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,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
|
+
|