@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,54 @@
1
+ export const name = "tdd"
2
+ export const description = "Start TDD workflow: scaffold → test → implement → refactor (usage: /tdd <feature description>)"
3
+
4
+ export async function run(ctx) {
5
+ const feature = (ctx.args || "").trim()
6
+
7
+ if (!feature) {
8
+ return `Please describe the feature to develop with TDD.
9
+
10
+ Usage: /tdd <feature description>
11
+
12
+ Examples:
13
+ /tdd add a user registration endpoint with email validation
14
+ /tdd implement a caching layer for API responses
15
+ /tdd create a file upload component with drag-and-drop`
16
+ }
17
+
18
+ return `Execute Test-Driven Development for the following feature:
19
+
20
+ **Feature**: ${feature}
21
+
22
+ Follow this strict TDD cycle:
23
+
24
+ ## Step 1: SCAFFOLD — Define interfaces
25
+ - Analyze the feature requirements
26
+ - Identify the public API: function signatures, types, interfaces
27
+ - Create empty implementation files with stub exports
28
+ - Verify the project compiles with stubs
29
+
30
+ ## Step 2: RED — Write failing tests FIRST
31
+ - Write tests that exercise the expected behavior
32
+ - Cover: happy path, edge cases, error conditions, boundary values
33
+ - Run tests to confirm they FAIL (this is critical — you must see red)
34
+ - Use the project's existing test framework (detect from package.json, pyproject.toml, go.mod, etc.)
35
+
36
+ ## Step 3: GREEN — Minimum code to pass
37
+ - Implement the SIMPLEST code that makes ALL tests pass
38
+ - Do NOT optimize or add features beyond what tests require
39
+ - Run tests after each implementation step
40
+
41
+ ## Step 4: REFACTOR — Improve while green
42
+ - Extract helpers, improve naming, reduce duplication
43
+ - Run tests AFTER EVERY refactoring step — they must stay green
44
+ - If a test breaks, undo the last change
45
+
46
+ ## Coverage Target: 80%+
47
+ Run coverage after completion: \`npx jest --coverage\`, \`pytest --cov\`, \`go test -cover\`, etc.
48
+
49
+ ## Report after each cycle:
50
+ 1. Tests written (count and names)
51
+ 2. Tests passing/failing
52
+ 3. Coverage percentage
53
+ 4. Decisions made during implementation`
54
+ }
@@ -0,0 +1,113 @@
1
+ import { writeFile, mkdir } from "node:fs/promises"
2
+ import { join, basename } from "node:path"
3
+ import { homedir } from "node:os"
4
+ import { requestProvider } from "../provider/router.mjs"
5
+
6
+ const SKILL_GEN_SYSTEM = `You are a skill generator for kkcode, a terminal AI coding agent.
7
+ Your task is to generate a skill file based on the user's description.
8
+
9
+ A skill is a reusable slash command. You can generate two types:
10
+
11
+ ## Type 1: Markdown Template (.md)
12
+ Simple prompt templates with variable expansion.
13
+ Variables: $ARGUMENTS, $CWD, $MODE, $PROJECT, $1, $2, ...
14
+
15
+ Example:
16
+ \`\`\`markdown
17
+ Review the code changes in $CWD and provide feedback.
18
+ Focus on: $ARGUMENTS
19
+ Project: $PROJECT
20
+ \`\`\`
21
+
22
+ ## Type 2: Programmable Skill (.mjs)
23
+ JavaScript module with full control. Must export: name, description, run(ctx).
24
+ ctx has: { args, cwd, mode, model, provider }
25
+
26
+ Example:
27
+ \`\`\`javascript
28
+ export const name = "test-coverage"
29
+ export const description = "Run tests and analyze coverage"
30
+
31
+ export async function run({ args, cwd }) {
32
+ return \`Run the test suite in \${cwd} and analyze code coverage.
33
+ Focus on: \${args || "all files"}
34
+ Report uncovered lines and suggest tests to add.\`
35
+ }
36
+ \`\`\`
37
+
38
+ ## Rules
39
+ - Choose .md for simple prompt templates, .mjs for anything needing logic
40
+ - The skill name should be kebab-case
41
+ - Keep the generated prompt focused and actionable
42
+ - Output ONLY the file content, no explanation
43
+ - First line must be a comment with the skill name: <!-- skill: name --> for .md, or // skill: name for .mjs`
44
+
45
+ /**
46
+ * Generate a skill file from a natural language description.
47
+ * Returns { name, filename, content, type } or null on failure.
48
+ */
49
+ export async function generateSkill({ description, configState, providerType, model, baseUrl, apiKeyEnv }) {
50
+ const response = await requestProvider({
51
+ configState,
52
+ providerType,
53
+ model,
54
+ system: SKILL_GEN_SYSTEM,
55
+ messages: [{ role: "user", content: `Create a skill for: ${description}` }],
56
+ tools: [],
57
+ baseUrl,
58
+ apiKeyEnv
59
+ })
60
+
61
+ const text = (response.text || "").trim()
62
+ if (!text) return null
63
+
64
+ // Detect type from content
65
+ const isMjs = text.includes("export ") || text.includes("export const") || text.includes("export async")
66
+ const type = isMjs ? "mjs" : "md"
67
+
68
+ // Extract skill name from first line comment
69
+ let name = null
70
+ const nameMatch = text.match(/(?:<!--\s*skill:\s*|\/\/\s*skill:\s*)([a-z0-9-]+)/i)
71
+ if (nameMatch) name = nameMatch[1].toLowerCase()
72
+
73
+ // Fallback: derive name from description
74
+ if (!name) {
75
+ name = description
76
+ .toLowerCase()
77
+ .replace(/[^a-z0-9\s-]/g, "")
78
+ .trim()
79
+ .replace(/\s+/g, "-")
80
+ .slice(0, 40)
81
+ if (!name) name = `skill-${Date.now()}`
82
+ }
83
+
84
+ // Strip markdown code fences if present
85
+ let content = text
86
+ const fenceMatch = content.match(/```(?:markdown|javascript|js|mjs)?\n([\s\S]*?)\n```/)
87
+ if (fenceMatch) content = fenceMatch[1]
88
+
89
+ const filename = `${name}.${type}`
90
+ return { name, filename, content, type }
91
+ }
92
+
93
+ /**
94
+ * Save a skill to the global skills directory.
95
+ */
96
+ export async function saveSkillGlobal(filename, content) {
97
+ const dir = join(homedir(), ".kkcode", "skills")
98
+ await mkdir(dir, { recursive: true })
99
+ const filePath = join(dir, filename)
100
+ await writeFile(filePath, content, "utf-8")
101
+ return filePath
102
+ }
103
+
104
+ /**
105
+ * Save a skill to the project skills directory.
106
+ */
107
+ export async function saveSkillProject(filename, content, cwd = process.cwd()) {
108
+ const dir = join(cwd, ".kkcode", "skills")
109
+ await mkdir(dir, { recursive: true })
110
+ const filePath = join(dir, filename)
111
+ await writeFile(filePath, content, "utf-8")
112
+ return filePath
113
+ }
@@ -0,0 +1,336 @@
1
+ import path from "node:path"
2
+ import { access, readdir, readFile } from "node:fs/promises"
3
+ import { pathToFileURL, fileURLToPath } from "node:url"
4
+ import { exec } from "node:child_process"
5
+ import { promisify } from "node:util"
6
+ import { parse as parseYaml } from "yaml"
7
+ import { McpRegistry } from "../mcp/registry.mjs"
8
+ import { loadCustomCommands, applyCommandTemplate } from "../command/custom-commands.mjs"
9
+
10
+ const execAsync = promisify(exec)
11
+
12
+ async function exists(target) {
13
+ try {
14
+ await access(target)
15
+ return true
16
+ } catch {
17
+ return false
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Parse YAML frontmatter from SKILL.md content.
23
+ * Returns { meta: {}, body: string }
24
+ */
25
+ function parseFrontmatter(raw) {
26
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/)
27
+ if (!match) return { meta: {}, body: raw.trim() }
28
+ try {
29
+ return { meta: parseYaml(match[1]) || {}, body: match[2].trim() }
30
+ } catch {
31
+ return { meta: {}, body: raw.trim() }
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Replace !`command` patterns with command stdout.
37
+ */
38
+ async function injectDynamicContext(template, cwd) {
39
+ const pattern = /!\`([^`]+)\`/g
40
+ const matches = [...template.matchAll(pattern)]
41
+ if (!matches.length) return template
42
+ let result = template
43
+ for (const m of matches) {
44
+ try {
45
+ const { stdout } = await execAsync(m[1], { cwd, timeout: 10000 })
46
+ result = result.replace(m[0], stdout.trim())
47
+ } catch {
48
+ result = result.replace(m[0], `[command failed: ${m[1]}]`)
49
+ }
50
+ }
51
+ return result
52
+ }
53
+
54
+ /**
55
+ * Load SKILL.md directory-format skills from a directory.
56
+ * Scans for <dir>/<name>/SKILL.md
57
+ */
58
+ async function loadAuxFiles(skillDir) {
59
+ const aux = {}
60
+ try {
61
+ const entries = await readdir(skillDir, { withFileTypes: true })
62
+ for (const e of entries) {
63
+ if (!e.isFile() || e.name === "SKILL.md") continue
64
+ aux[e.name] = path.join(skillDir, e.name)
65
+ }
66
+ } catch { /* ignore */ }
67
+ return aux
68
+ }
69
+
70
+ async function loadSkillDirs(dir, scope) {
71
+ if (!(await exists(dir))) return []
72
+ const entries = await readdir(dir, { withFileTypes: true })
73
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort()
74
+ const skills = []
75
+ for (const name of dirs) {
76
+ const skillDir = path.join(dir, name)
77
+ const mdPath = path.join(skillDir, "SKILL.md")
78
+ if (!(await exists(mdPath))) continue
79
+ try {
80
+ const raw = await readFile(mdPath, "utf8")
81
+ const { meta, body } = parseFrontmatter(raw)
82
+ const auxFiles = await loadAuxFiles(skillDir)
83
+ skills.push({
84
+ name: meta.name || name,
85
+ description: meta.description || name,
86
+ type: "skill_md",
87
+ scope,
88
+ source: mdPath,
89
+ skillDir,
90
+ template: body,
91
+ auxFiles,
92
+ disableModelInvocation: !!meta["disable-model-invocation"],
93
+ userInvocable: meta["user-invocable"] !== false,
94
+ allowedTools: meta["allowed-tools"] || null,
95
+ model: meta.model || null,
96
+ contextFork: !!meta["context-fork"]
97
+ })
98
+ } catch { /* skip broken */ }
99
+ }
100
+ return skills
101
+ }
102
+
103
+ /**
104
+ * Load .mjs programmable skills from a directory.
105
+ * Each .mjs file should export: { name, description, run(ctx) }
106
+ * run() returns a string prompt to send to the model.
107
+ */
108
+ async function loadMjsSkills(dir, scope) {
109
+ if (!(await exists(dir))) return []
110
+ const entries = await readdir(dir, { withFileTypes: true })
111
+ const files = entries
112
+ .filter((e) => e.isFile() && e.name.endsWith(".mjs"))
113
+ .map((e) => e.name)
114
+ .sort()
115
+
116
+ const skills = []
117
+ for (const file of files) {
118
+ const full = path.join(dir, file)
119
+ try {
120
+ const mod = await import(pathToFileURL(full).href)
121
+ const name = mod.name || path.basename(file, ".mjs")
122
+ skills.push({
123
+ name,
124
+ description: mod.description || name,
125
+ type: "mjs",
126
+ scope,
127
+ source: full,
128
+ run: typeof mod.run === "function" ? mod.run : null
129
+ })
130
+ } catch {
131
+ // Skip broken skill files silently
132
+ }
133
+ }
134
+ return skills
135
+ }
136
+
137
+ /**
138
+ * Convert custom commands (.md templates) to skill format.
139
+ */
140
+ function customCommandsToSkills(commands) {
141
+ return commands.map((cmd) => ({
142
+ name: cmd.name,
143
+ description: `custom command (${cmd.scope})`,
144
+ type: "template",
145
+ scope: cmd.scope,
146
+ source: cmd.source,
147
+ template: cmd.template
148
+ }))
149
+ }
150
+
151
+ /**
152
+ * Convert MCP prompts to skill format.
153
+ */
154
+ function mcpPromptsToSkills(prompts) {
155
+ return prompts.map((p) => ({
156
+ name: p.name,
157
+ description: p.description || `${p.server}:${p.name}`,
158
+ type: "mcp_prompt",
159
+ scope: "mcp",
160
+ server: p.server,
161
+ promptId: p.id,
162
+ arguments: p.arguments || []
163
+ }))
164
+ }
165
+
166
+ const state = {
167
+ skills: new Map(),
168
+ loaded: false
169
+ }
170
+
171
+ export const SkillRegistry = {
172
+ /**
173
+ * Load all skills from all sources.
174
+ */
175
+ async initialize(config, cwd = process.cwd()) {
176
+ state.skills.clear()
177
+
178
+ // Source 0: Built-in skills (shipped with kkcode)
179
+ const builtinDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "builtin")
180
+ const builtinSkills = await loadMjsSkills(builtinDir, "builtin")
181
+ for (const skill of builtinSkills) {
182
+ state.skills.set(skill.name, skill)
183
+ }
184
+
185
+ // Source 1: Custom commands (.md templates)
186
+ const customCommands = await loadCustomCommands(cwd)
187
+ for (const skill of customCommandsToSkills(customCommands)) {
188
+ state.skills.set(skill.name, skill)
189
+ }
190
+
191
+ // Source 2: Programmable skills (.mjs)
192
+ const userRoot = process.env.USERPROFILE || process.env.HOME || cwd
193
+ const globalSkillDir = path.join(userRoot, ".kkcode", "skills")
194
+ const projectSkillDir = path.join(cwd, ".kkcode", "skills")
195
+ const [globalSkills, projectSkills, globalSkillMds, projectSkillMds] = await Promise.all([
196
+ loadMjsSkills(globalSkillDir, "global"),
197
+ loadMjsSkills(projectSkillDir, "project"),
198
+ loadSkillDirs(globalSkillDir, "global"),
199
+ loadSkillDirs(projectSkillDir, "project")
200
+ ])
201
+ // Project skills override global skills with same name
202
+ for (const skill of [...globalSkills, ...projectSkills, ...globalSkillMds, ...projectSkillMds]) {
203
+ state.skills.set(skill.name, skill)
204
+ }
205
+
206
+ // Source 3: MCP prompts (if MCP is initialized)
207
+ if (McpRegistry.isReady()) {
208
+ const prompts = McpRegistry.listPrompts()
209
+ for (const skill of mcpPromptsToSkills(prompts)) {
210
+ // Prefix MCP skills to avoid name collisions
211
+ const key = `mcp:${skill.name}`
212
+ state.skills.set(key, { ...skill, name: key })
213
+ }
214
+ }
215
+
216
+ state.loaded = true
217
+ },
218
+
219
+ isReady() {
220
+ return state.loaded
221
+ },
222
+
223
+ list() {
224
+ return [...state.skills.values()]
225
+ },
226
+
227
+ get(name) {
228
+ return state.skills.get(name) || null
229
+ },
230
+
231
+ /**
232
+ * Execute a skill and return the expanded prompt string.
233
+ */
234
+ async execute(name, args = "", context = {}) {
235
+ const skill = state.skills.get(name)
236
+ if (!skill) return null
237
+
238
+ if (skill.type === "mjs" && skill.run) {
239
+ // Programmable skill — call run() to get prompt
240
+ try {
241
+ const result = await skill.run({
242
+ args,
243
+ cwd: context.cwd || process.cwd(),
244
+ mode: context.mode || "agent",
245
+ model: context.model || "",
246
+ provider: context.provider || ""
247
+ })
248
+ return result == null ? "" : typeof result === "string" ? result : JSON.stringify(result)
249
+ } catch (error) {
250
+ return `skill execution error (${name}): ${error?.message || String(error)}`
251
+ }
252
+ }
253
+
254
+ if (skill.type === "template" && skill.template) {
255
+ // Template skill — expand $ARGUMENTS, $1, $2, etc.
256
+ return applyCommandTemplate(skill.template, args, {
257
+ path: context.cwd || process.cwd(),
258
+ mode: context.mode || "agent",
259
+ provider: context.provider || "",
260
+ cwd: context.cwd || process.cwd(),
261
+ project: path.basename(context.cwd || process.cwd())
262
+ })
263
+ }
264
+
265
+ if (skill.type === "skill_md" && skill.template) {
266
+ const cwd = context.cwd || process.cwd()
267
+ let prompt = applyCommandTemplate(skill.template, args, {
268
+ path: cwd, mode: context.mode || "agent",
269
+ provider: context.provider || "", cwd, project: path.basename(cwd)
270
+ })
271
+ // Resolve $FILE{name} references to auxiliary file contents
272
+ if (skill.auxFiles) {
273
+ const filePattern = /\$FILE\{([^}]+)\}/g
274
+ const fileMatches = [...prompt.matchAll(filePattern)]
275
+ for (const m of fileMatches) {
276
+ const filePath = skill.auxFiles[m[1]]
277
+ if (filePath) {
278
+ try {
279
+ const content = await readFile(filePath, "utf8")
280
+ prompt = prompt.replace(m[0], content.trim())
281
+ } catch {
282
+ prompt = prompt.replace(m[0], `[file not found: ${m[1]}]`)
283
+ }
284
+ }
285
+ }
286
+ }
287
+ prompt = await injectDynamicContext(prompt, cwd)
288
+ if (skill.contextFork) {
289
+ return { prompt, contextFork: true, model: skill.model }
290
+ }
291
+ return prompt
292
+ }
293
+
294
+ if (skill.type === "mcp_prompt" && skill.promptId) {
295
+ // MCP prompt — fetch from server
296
+ const promptArgs = {}
297
+ if (args) {
298
+ // Simple: pass entire args string as first argument
299
+ const argDefs = skill.arguments || []
300
+ if (argDefs.length === 1) {
301
+ promptArgs[argDefs[0].name] = args
302
+ } else if (argDefs.length > 1) {
303
+ // Split args by spaces for multiple arguments
304
+ const tokens = args.split(/\s+/)
305
+ for (let i = 0; i < argDefs.length && i < tokens.length; i++) {
306
+ promptArgs[argDefs[i].name] = tokens[i]
307
+ }
308
+ }
309
+ }
310
+ const result = await McpRegistry.getPrompt(skill.promptId, promptArgs)
311
+ // MCP prompt result: { messages: [{ role, content: { type, text } }] }
312
+ if (result?.messages) {
313
+ return result.messages
314
+ .map((m) => {
315
+ if (typeof m.content === "string") return m.content
316
+ if (m.content?.text) return m.content.text
317
+ return ""
318
+ })
319
+ .filter(Boolean)
320
+ .join("\n\n")
321
+ }
322
+ return JSON.stringify(result)
323
+ }
324
+
325
+ return null
326
+ },
327
+
328
+ /**
329
+ * Return skill metadata for system prompt inclusion.
330
+ */
331
+ listForSystemPrompt() {
332
+ return [...state.skills.values()]
333
+ .filter((s) => !s.disableModelInvocation)
334
+ .map((s) => ({ name: s.name, description: s.description }))
335
+ }
336
+ }
@@ -0,0 +1,83 @@
1
+ import { ensureUserRoot, auditStorePath } from "./paths.mjs"
2
+ import { readJson, writeJson } from "./json-store.mjs"
3
+
4
+ const state = {
5
+ maxEntries: 5000
6
+ }
7
+
8
+ function defaults() {
9
+ return {
10
+ updatedAt: Date.now(),
11
+ entries: []
12
+ }
13
+ }
14
+
15
+ export function configureAuditStore(options = {}) {
16
+ if (Number.isInteger(options.maxEntries) && options.maxEntries > 100) {
17
+ state.maxEntries = options.maxEntries
18
+ }
19
+ }
20
+
21
+ export async function readAuditStore() {
22
+ await ensureUserRoot()
23
+ return readJson(auditStorePath(), defaults())
24
+ }
25
+
26
+ export async function appendAuditEntry(entry) {
27
+ const store = await readAuditStore()
28
+ store.entries.push({
29
+ id: `aud_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
30
+ createdAt: Date.now(),
31
+ ...entry
32
+ })
33
+ if (store.entries.length > state.maxEntries) {
34
+ store.entries = store.entries.slice(-state.maxEntries)
35
+ }
36
+ store.updatedAt = Date.now()
37
+ await writeJson(auditStorePath(), store)
38
+ return store.entries[store.entries.length - 1]
39
+ }
40
+
41
+ export async function listAuditEntries(options = {}) {
42
+ const store = await readAuditStore()
43
+
44
+ const query = typeof options === "number" ? { limit: options } : options
45
+ const limit = Math.max(1, Number(query.limit || 200))
46
+ const sessionId = query.sessionId || null
47
+ const tool = query.tool || null
48
+ const type = query.type || null
49
+ const sinceMs = query.sinceMs || null
50
+
51
+ const list = store.entries.filter((entry) => {
52
+ if (sessionId && entry.sessionId !== sessionId) return false
53
+ if (tool && entry.tool !== tool) return false
54
+ if (type && entry.type !== type) return false
55
+ if (sinceMs && entry.createdAt < sinceMs) return false
56
+ return true
57
+ })
58
+
59
+ return list.slice(-limit).reverse()
60
+ }
61
+
62
+ export async function auditStats() {
63
+ const store = await readAuditStore()
64
+ const now = Date.now()
65
+ const oneHour = now - 60 * 60 * 1000
66
+ const oneDay = now - 24 * 60 * 60 * 1000
67
+
68
+ let error1h = 0
69
+ let error24h = 0
70
+ for (const entry of store.entries) {
71
+ const isError = String(entry.type || "").includes("error") || entry.ok === false
72
+ if (!isError) continue
73
+ if (entry.createdAt >= oneHour) error1h += 1
74
+ if (entry.createdAt >= oneDay) error24h += 1
75
+ }
76
+
77
+ return {
78
+ total: store.entries.length,
79
+ error1h,
80
+ error24h,
81
+ maxEntries: state.maxEntries
82
+ }
83
+ }
@@ -0,0 +1,82 @@
1
+ import path from "node:path"
2
+ import { appendFile, readdir, rename, stat, unlink } from "node:fs/promises"
3
+ import { ensureUserRoot, eventLogPath, userRootDir } from "./paths.mjs"
4
+
5
+ const state = {
6
+ rotateMb: 32,
7
+ retainDays: 14
8
+ }
9
+
10
+ function now() {
11
+ return Date.now()
12
+ }
13
+
14
+ function maxBytes() {
15
+ return Math.max(1, Number(state.rotateMb || 32)) * 1024 * 1024
16
+ }
17
+
18
+ export function configureEventLog(options = {}) {
19
+ if (Number.isFinite(options.rotateMb) && options.rotateMb > 0) state.rotateMb = Number(options.rotateMb)
20
+ if (Number.isFinite(options.retainDays) && options.retainDays > 0) state.retainDays = Number(options.retainDays)
21
+ }
22
+
23
+ async function maybeRotate() {
24
+ const file = eventLogPath()
25
+ const info = await stat(file).catch(() => null)
26
+ if (!info || info.size < maxBytes()) return
27
+ const rotated = path.join(userRootDir(), `events.${now()}.log`)
28
+ await rename(file, rotated).catch(() => {})
29
+ }
30
+
31
+ async function cleanupOldLogs() {
32
+ const cutoff = now() - Math.max(1, Number(state.retainDays || 14)) * 24 * 60 * 60 * 1000
33
+ const entries = await readdir(userRootDir(), { withFileTypes: true }).catch(() => [])
34
+ for (const entry of entries) {
35
+ if (!entry.isFile()) continue
36
+ if (!entry.name.startsWith("events.") || !entry.name.endsWith(".log")) continue
37
+ const file = path.join(userRootDir(), entry.name)
38
+ const info = await stat(file).catch(() => null)
39
+ if (!info) continue
40
+ if (info.mtimeMs < cutoff) {
41
+ await unlink(file).catch(() => {})
42
+ }
43
+ }
44
+ }
45
+
46
+ export async function appendEventLog(event) {
47
+ await ensureUserRoot()
48
+ await maybeRotate()
49
+ await appendFile(eventLogPath(), JSON.stringify(event) + "\n", "utf8")
50
+ await cleanupOldLogs()
51
+ }
52
+
53
+ export async function eventLogStats() {
54
+ await ensureUserRoot()
55
+ const root = userRootDir()
56
+ const entries = await readdir(root, { withFileTypes: true }).catch(() => [])
57
+ let activeBytes = 0
58
+ let rotatedBytes = 0
59
+ let rotatedFiles = 0
60
+
61
+ for (const entry of entries) {
62
+ if (!entry.isFile()) continue
63
+ if (entry.name === "events.log") {
64
+ const info = await stat(path.join(root, entry.name)).catch(() => null)
65
+ if (info) activeBytes += info.size
66
+ continue
67
+ }
68
+ if (entry.name.startsWith("events.") && entry.name.endsWith(".log")) {
69
+ rotatedFiles += 1
70
+ const info = await stat(path.join(root, entry.name)).catch(() => null)
71
+ if (info) rotatedBytes += info.size
72
+ }
73
+ }
74
+
75
+ return {
76
+ rotateMb: state.rotateMb,
77
+ retainDays: state.retainDays,
78
+ activeBytes,
79
+ rotatedFiles,
80
+ rotatedBytes
81
+ }
82
+ }