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