@kkelly-offical/kkcode 0.1.6 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +452 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +19 -2
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +90 -0
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/config/defaults.mjs +280 -260
  29. package/src/config/import-config.mjs +1 -1
  30. package/src/config/load-config.mjs +61 -4
  31. package/src/config/schema.mjs +591 -574
  32. package/src/context.mjs +4 -1
  33. package/src/core/constants.mjs +97 -91
  34. package/src/core/types.mjs +1 -1
  35. package/src/github/api.mjs +78 -78
  36. package/src/github/auth.mjs +294 -286
  37. package/src/github/flow.mjs +298 -298
  38. package/src/github/workspace.mjs +225 -212
  39. package/src/index.mjs +84 -82
  40. package/src/knowledge/frontend-aesthetics.txt +38 -38
  41. package/src/mcp/client-http.mjs +139 -141
  42. package/src/mcp/client-sse.mjs +297 -288
  43. package/src/mcp/client-stdio.mjs +534 -533
  44. package/src/mcp/constants.mjs +2 -2
  45. package/src/mcp/registry.mjs +498 -479
  46. package/src/mcp/stdio-framing.mjs +135 -133
  47. package/src/mcp/tool-result.mjs +24 -24
  48. package/src/observability/edit-diagnostics.mjs +449 -0
  49. package/src/observability/index.mjs +42 -42
  50. package/src/observability/metrics.mjs +165 -137
  51. package/src/observability/tracer.mjs +137 -137
  52. package/src/onboarding.mjs +209 -0
  53. package/src/orchestration/background-manager.mjs +567 -372
  54. package/src/orchestration/background-worker.mjs +419 -305
  55. package/src/orchestration/interruption-reason.mjs +21 -0
  56. package/src/orchestration/longagent-manager.mjs +197 -171
  57. package/src/orchestration/stage-scheduler.mjs +733 -728
  58. package/src/orchestration/subagent-router.mjs +7 -1
  59. package/src/orchestration/task-scheduler.mjs +219 -7
  60. package/src/permission/engine.mjs +1 -1
  61. package/src/permission/exec-policy.mjs +370 -370
  62. package/src/permission/file-edit-policy.mjs +108 -0
  63. package/src/permission/prompt.mjs +1 -1
  64. package/src/permission/rules.mjs +116 -7
  65. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  66. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  67. package/src/plugin/hook-bus.mjs +19 -5
  68. package/src/plugin/manifest-loader.mjs +222 -0
  69. package/src/provider/anthropic.mjs +396 -390
  70. package/src/provider/ollama.mjs +7 -1
  71. package/src/provider/openai.mjs +382 -340
  72. package/src/provider/retry-policy.mjs +74 -68
  73. package/src/provider/router.mjs +242 -241
  74. package/src/provider/sse.mjs +104 -104
  75. package/src/provider/wizard.mjs +556 -0
  76. package/src/repl/capability-facade.mjs +30 -0
  77. package/src/repl/command-surface.mjs +23 -0
  78. package/src/repl/controller-entry.mjs +40 -0
  79. package/src/repl/core-shell.mjs +208 -0
  80. package/src/repl/dialog-router.mjs +87 -0
  81. package/src/repl/input-engine.mjs +76 -0
  82. package/src/repl/keymap.mjs +7 -0
  83. package/src/repl/operator-surface.mjs +15 -0
  84. package/src/repl/permission-flow.mjs +49 -0
  85. package/src/repl/runtime-facade.mjs +36 -0
  86. package/src/repl/slash-router.mjs +62 -0
  87. package/src/repl/state-store.mjs +29 -0
  88. package/src/repl/turn-controller.mjs +58 -0
  89. package/src/repl/verification.mjs +23 -0
  90. package/src/repl.mjs +3368 -2929
  91. package/src/rules/load-rules.mjs +3 -3
  92. package/src/runtime.mjs +1 -1
  93. package/src/session/agent-transaction.mjs +86 -0
  94. package/src/session/checkpoint.mjs +302 -302
  95. package/src/session/compaction.mjs +36 -14
  96. package/src/session/engine.mjs +417 -227
  97. package/src/session/longagent-4stage.mjs +467 -460
  98. package/src/session/longagent-hybrid.mjs +1344 -1081
  99. package/src/session/longagent-plan.mjs +376 -365
  100. package/src/session/longagent-project-memory.mjs +53 -53
  101. package/src/session/longagent-scaffold.mjs +291 -291
  102. package/src/session/longagent-task-bus.mjs +138 -54
  103. package/src/session/longagent-utils.mjs +828 -472
  104. package/src/session/longagent.mjs +911 -884
  105. package/src/session/loop.mjs +1005 -905
  106. package/src/session/prompt/agent.txt +25 -0
  107. package/src/session/prompt/anthropic.txt +150 -150
  108. package/src/session/prompt/beast.txt +1 -1
  109. package/src/session/prompt/plan.txt +28 -6
  110. package/src/session/prompt/qwen.txt +46 -46
  111. package/src/session/recovery.mjs +21 -0
  112. package/src/session/rollback.mjs +197 -0
  113. package/src/session/routing-observability.mjs +72 -0
  114. package/src/session/runtime-state.mjs +47 -0
  115. package/src/session/store.mjs +523 -510
  116. package/src/session/system-prompt.mjs +56 -8
  117. package/src/session/task-validator.mjs +267 -267
  118. package/src/session/usability-gates.mjs +2 -2
  119. package/src/skill/builtin/commit.mjs +64 -64
  120. package/src/skill/builtin/design.mjs +76 -76
  121. package/src/skill/generator.mjs +18 -2
  122. package/src/skill/registry.mjs +642 -390
  123. package/src/storage/audit-store.mjs +18 -11
  124. package/src/storage/event-log.mjs +7 -1
  125. package/src/storage/ghost-commit-store.mjs +243 -245
  126. package/src/storage/paths.mjs +13 -0
  127. package/src/theme/default-theme.mjs +1 -1
  128. package/src/theme/markdown.mjs +4 -0
  129. package/src/theme/schema.mjs +1 -1
  130. package/src/theme/status-bar.mjs +162 -158
  131. package/src/tool/audit-wrapper.mjs +18 -2
  132. package/src/tool/edit-transaction.mjs +23 -0
  133. package/src/tool/executor.mjs +26 -1
  134. package/src/tool/file-read-state.mjs +65 -0
  135. package/src/tool/git-auto.mjs +526 -526
  136. package/src/tool/git-full-auto.mjs +487 -478
  137. package/src/tool/mutation-guard.mjs +54 -0
  138. package/src/tool/prompt/edit.txt +3 -3
  139. package/src/tool/prompt/multiedit.txt +1 -0
  140. package/src/tool/prompt/notebookedit.txt +2 -1
  141. package/src/tool/prompt/patch.txt +25 -24
  142. package/src/tool/prompt/read.txt +3 -3
  143. package/src/tool/prompt/sysinfo.txt +29 -0
  144. package/src/tool/prompt/task.txt +66 -4
  145. package/src/tool/prompt/write.txt +2 -2
  146. package/src/tool/question-prompt.mjs +17 -4
  147. package/src/tool/registry.mjs +1701 -1343
  148. package/src/tool/task-tool.mjs +14 -6
  149. package/src/ui/activity-renderer.mjs +667 -664
  150. package/src/ui/repl-background-panel.mjs +7 -0
  151. package/src/ui/repl-capability-panel.mjs +9 -0
  152. package/src/ui/repl-dashboard.mjs +54 -4
  153. package/src/ui/repl-help.mjs +110 -0
  154. package/src/ui/repl-operator-panel.mjs +12 -0
  155. package/src/ui/repl-route-feedback.mjs +35 -0
  156. package/src/ui/repl-status-view.mjs +76 -0
  157. package/src/ui/repl-task-panel.mjs +5 -0
  158. package/src/ui/repl-transcript-panel.mjs +56 -0
  159. package/src/ui/repl-turn-summary.mjs +135 -0
  160. package/src/usage/pricing.mjs +122 -121
  161. package/src/usage/usage-meter.mjs +1 -0
  162. package/src/util/git.mjs +562 -519
  163. package/src/util/template.mjs +6 -1
@@ -1,68 +1,109 @@
1
- import { Command } from "commander"
2
- import { buildContext, printContextWarnings } from "../context.mjs"
3
- import { executeTurn, newSessionId, resolveMode } from "../session/engine.mjs"
4
- import { renderStatusBar } from "../theme/status-bar.mjs"
1
+ import { Command } from "commander"
2
+ import { buildContext, printContextWarnings } from "../context.mjs"
3
+ import { ensureEventSinks, executeTurn, formatPublicModeSummary, getPublicModeContract, newSessionId, resolveMode, resolvePromptMode, summarizeRouteDecision } from "../session/engine.mjs"
4
+ import { emitRouteDecisionEvent } from "../session/routing-observability.mjs"
5
+ import { renderStatusBar } from "../theme/status-bar.mjs"
5
6
  import { applyCommandTemplate, loadCustomCommands } from "../command/custom-commands.mjs"
6
7
  import { ToolRegistry } from "../tool/registry.mjs"
8
+ import { SkillRegistry } from "../skill/registry.mjs"
9
+ import { PermissionEngine } from "../permission/engine.mjs"
7
10
  import { HookBus, initHookBus } from "../plugin/hook-bus.mjs"
8
11
  import { listProviders } from "../provider/router.mjs"
12
+ import { EventBus } from "../core/events.mjs"
13
+ import { EVENT_TYPES } from "../core/constants.mjs"
14
+
15
+ export function resolveChatExecutionMode(prompt, requestedMode) {
16
+ return resolvePromptMode(prompt, requestedMode)
17
+ }
9
18
 
10
19
  export function createChatCommand() {
11
20
  const providers = listProviders()
12
21
  return new Command("chat")
13
- .description("run one prompt in ask/plan/agent/longagent mode")
22
+ .description("run one prompt in assistant/plan/agent/code/longagent mode (assistant = default personal lane)")
14
23
  .argument("<prompt...>", "prompt text")
15
- .option("--mode <mode>", "ask|plan|agent|longagent", "agent")
24
+ .option("--mode <mode>", "assistant|plan|agent|code|coding|longagent", "assistant")
16
25
  .option("--model <model>", "model id")
17
26
  .option("--provider-type <type>", `provider type (${providers.join("|")})`)
18
- .option("--base-url <url>", "provider base url override")
19
- .option("--api-key-env <name>", "api key env override")
20
- .option("--max-iterations <n>", "longagent max iterations (0 = unlimited)")
21
- .option("--session <id>", "session id")
22
- .action(async (promptParts, options) => {
23
- const ctx = await buildContext()
24
- printContextWarnings(ctx)
25
- let prompt = promptParts.join(" ").trim()
26
- if (prompt.startsWith("/")) {
27
- const commands = await loadCustomCommands(process.cwd())
28
- const [name, ...argTokens] = prompt.slice(1).split(/\s+/)
29
- const custom = commands.find((item) => item.name === name)
30
- if (custom) {
31
- const args = argTokens.join(" ").trim()
32
- prompt = applyCommandTemplate(custom.template, args, {
33
- path: process.cwd()
34
- })
35
- }
36
- }
37
-
38
- const mode = resolveMode(options.mode)
39
- const providerType = options.providerType ?? ctx.configState.config.provider.default
40
- const providerDefaults = ctx.configState.config.provider[providerType]
41
- if (!providerDefaults) {
42
- throw new Error(`unknown provider type: ${providerType}`)
43
- }
44
- const model = options.model ?? providerDefaults.default_model
45
- const sessionId = options.session || newSessionId()
46
-
47
- await ToolRegistry.initialize({
48
- config: ctx.configState.config,
49
- cwd: process.cwd()
50
- })
51
-
52
- await initHookBus()
53
- const chatParams = await HookBus.chatParams({
54
- prompt,
55
- mode,
56
- model,
57
- providerType,
58
- sessionId,
59
- baseUrl: options.baseUrl ?? null,
60
- apiKeyEnv: options.apiKeyEnv ?? null
61
- })
62
-
27
+ .option("--base-url <url>", "provider base url override")
28
+ .option("--api-key-env <name>", "api key env override")
29
+ .option("--max-iterations <n>", "longagent max iterations (0 = unlimited)")
30
+ .option("--session <id>", "session id")
31
+ .action(async (promptParts, options) => {
32
+ const ctx = await buildContext()
33
+ printContextWarnings(ctx)
34
+ PermissionEngine.setTrusted(ctx.trustState?.trusted !== false)
35
+ let prompt = promptParts.join(" ").trim()
36
+ if (prompt.startsWith("/")) {
37
+ const commands = await loadCustomCommands(process.cwd())
38
+ const [name, ...argTokens] = prompt.slice(1).split(/\s+/)
39
+ const custom = commands.find((item) => item.name === name)
40
+ if (custom) {
41
+ const args = argTokens.join(" ").trim()
42
+ prompt = applyCommandTemplate(custom.template, args, {
43
+ path: process.cwd()
44
+ })
45
+ }
46
+ }
47
+
48
+ const mode = resolveMode(options.mode)
49
+ const providerType = options.providerType ?? ctx.configState.config.provider.default
50
+ const providerDefaults = ctx.configState.config.provider[providerType]
51
+ if (!providerDefaults) {
52
+ throw new Error(`unknown provider type: ${providerType}`)
53
+ }
54
+ const model = options.model ?? providerDefaults.default_model
55
+ const sessionId = options.session || newSessionId()
56
+
57
+ await ToolRegistry.initialize({
58
+ config: ctx.configState.config,
59
+ cwd: process.cwd()
60
+ })
61
+ await SkillRegistry.initialize(ctx.configState.config, process.cwd())
62
+
63
+ await initHookBus()
64
+ const chatParams = await HookBus.chatParams({
65
+ prompt,
66
+ mode,
67
+ model,
68
+ providerType,
69
+ sessionId,
70
+ baseUrl: options.baseUrl ?? null,
71
+ apiKeyEnv: options.apiKeyEnv ?? null
72
+ })
73
+
74
+ const routedMode = resolveChatExecutionMode(chatParams.prompt ?? prompt, chatParams.mode ?? mode)
75
+ const effectiveMode = routedMode.effectiveMode
76
+ const effectiveExplanation = routedMode.route.explanation || routedMode.route.reason
77
+ const requestedContract = getPublicModeContract(routedMode.requestedMode)
78
+ const effectiveContract = routedMode.effectiveContract || getPublicModeContract(effectiveMode)
79
+ ensureEventSinks()
80
+ await emitRouteDecisionEvent({
81
+ sessionId,
82
+ source: "chat",
83
+ requestedMode: routedMode.requestedMode,
84
+ route: routedMode.route,
85
+ prompt: chatParams.prompt ?? prompt
86
+ })
87
+
88
+ if (routedMode.route.changed) {
89
+ console.log(`mode routed: ${routedMode.requestedMode} -> ${effectiveMode} (${effectiveExplanation})`)
90
+ } else if (routedMode.route.forced && routedMode.route.suggestion) {
91
+ console.log(`mode kept: ${effectiveMode} (${effectiveExplanation}; suggested ${routedMode.route.suggestion})`)
92
+ } else if (routedMode.route.suggestion === "longagent" && (routedMode.requestedMode === "assistant" || routedMode.requestedMode === "agent")) {
93
+ console.log(`mode note: ${effectiveMode} (${effectiveExplanation}; consider --mode longagent)`)
94
+ } else {
95
+ console.log(`mode: ${effectiveMode} (${effectiveExplanation})`)
96
+ }
97
+ if (routedMode.requestedMode !== effectiveMode) {
98
+ console.log(`requested lane: ${requestedContract.summary}`)
99
+ }
100
+ console.log(`effective lane: ${formatPublicModeSummary(effectiveMode)}`)
101
+ console.log(`lane guarantee: ${effectiveContract.guarantee}`)
102
+ console.log(`route summary: ${summarizeRouteDecision(routedMode.route)}`)
103
+
63
104
  const result = await executeTurn({
64
105
  prompt: chatParams.prompt ?? prompt,
65
- mode: chatParams.mode ?? mode,
106
+ mode: effectiveMode,
66
107
  model: chatParams.model ?? model,
67
108
  sessionId,
68
109
  configState: ctx.configState,
@@ -74,41 +115,41 @@ export function createChatCommand() {
74
115
  ? { write: (chunk) => process.stdout.write(String(chunk || "")) }
75
116
  : null
76
117
  })
77
-
118
+
78
119
  const status = renderStatusBar({
79
- mode,
120
+ mode: effectiveMode,
80
121
  model: result.model,
81
- permission: ctx.configState.config.permission.default_policy,
122
+ permission: ctx.configState.config.permission.mode || ctx.configState.config.permission.default_policy,
82
123
  tokenMeter: result.tokenMeter,
83
- aggregation: ctx.configState.config.usage.aggregation,
84
- cost: result.cost,
124
+ aggregation: ctx.configState.config.usage.aggregation,
125
+ cost: result.cost,
85
126
  showCost: ctx.configState.config.ui.status.show_cost,
86
127
  showTokenMeter: ctx.configState.config.ui.status.show_token_meter,
87
128
  theme: ctx.themeState.theme,
88
129
  layout: ctx.configState.config.ui.layout,
89
- longagentState: mode === "longagent" ? result.longagent : null
130
+ longagentState: effectiveMode === "longagent" ? result.longagent : null
90
131
  })
91
-
132
+
92
133
  console.log(status)
93
134
  console.log("")
94
135
  const streamEnabled = ctx.configState.config.provider[chatParams.providerType ?? providerType]?.stream !== false
95
136
  if (!streamEnabled || !result.emittedText) {
96
137
  console.log(result.reply)
97
138
  }
98
- console.log("")
99
- if (result.toolEvents.length) {
100
- console.log(`tool events: ${result.toolEvents.length}`)
101
- }
102
- if (result.pricingWarnings.length) {
103
- for (const warning of result.pricingWarnings) {
104
- console.log(`pricing warning: ${warning}`)
105
- }
106
- }
107
- if (result.budgetWarnings.length) {
108
- for (const warning of result.budgetWarnings) {
109
- console.log(`budget warning: ${warning}`)
110
- }
111
- }
112
- console.log(`session: ${sessionId}`)
113
- })
114
- }
139
+ console.log("")
140
+ if (result.toolEvents.length) {
141
+ console.log(`tool events: ${result.toolEvents.length}`)
142
+ }
143
+ if (result.pricingWarnings.length) {
144
+ for (const warning of result.pricingWarnings) {
145
+ console.log(`pricing warning: ${warning}`)
146
+ }
147
+ }
148
+ if (result.budgetWarnings.length) {
149
+ for (const warning of result.budgetWarnings) {
150
+ console.log(`budget warning: ${warning}`)
151
+ }
152
+ }
153
+ console.log(`session: ${sessionId}`)
154
+ })
155
+ }
@@ -1,9 +1,12 @@
1
1
  import path from "node:path"
2
- import { readFile, writeFile } from "node:fs/promises"
2
+ import { readFile, writeFile, mkdir } from "node:fs/promises"
3
3
  import { Command } from "commander"
4
4
  import YAML from "yaml"
5
5
  import { importConfig } from "../config/import-config.mjs"
6
6
  import { validateConfig } from "../config/schema.mjs"
7
+ import { loadConfig } from "../config/load-config.mjs"
8
+ import { DEFAULT_CONFIG } from "../config/defaults.mjs"
9
+ import { projectConfigCandidates } from "../storage/paths.mjs"
7
10
 
8
11
  function parseInput(file, raw) {
9
12
  if (file.endsWith(".json")) return JSON.parse(raw)
@@ -15,6 +18,72 @@ function stringifyOutput(file, data) {
15
18
  return YAML.stringify(data)
16
19
  }
17
20
 
21
+ function getByPath(obj, keyPath) {
22
+ const keys = keyPath.split(".")
23
+ let current = obj
24
+ for (const key of keys) {
25
+ if (current == null || typeof current !== "object") return undefined
26
+ current = current[key]
27
+ }
28
+ return current
29
+ }
30
+
31
+ const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"])
32
+
33
+ function setByPath(obj, keyPath, value) {
34
+ const keys = keyPath.split(".")
35
+ if (keys.some(k => FORBIDDEN_KEYS.has(k))) {
36
+ throw new Error(`forbidden key in path: ${keyPath}`)
37
+ }
38
+ let current = obj
39
+ for (let i = 0; i < keys.length - 1; i++) {
40
+ const key = keys[i]
41
+ if (current[key] == null || typeof current[key] !== "object") {
42
+ current[key] = {}
43
+ }
44
+ current = current[key]
45
+ }
46
+ current[keys[keys.length - 1]] = value
47
+ }
48
+
49
+ function coerceValue(raw) {
50
+ if (raw === "true") return true
51
+ if (raw === "false") return false
52
+ if (raw === "null") return null
53
+ if (raw !== "" && /^-?\d+(\.\d+)?$/.test(raw)) return Number(raw)
54
+ return raw
55
+ }
56
+
57
+ function isNonDefault(merged, defaults, keyPath) {
58
+ const mergedVal = getByPath(merged, keyPath)
59
+ const defaultVal = getByPath(defaults, keyPath)
60
+ return JSON.stringify(mergedVal) !== JSON.stringify(defaultVal)
61
+ }
62
+
63
+ function printTree(obj, defaults, prefix = "", indent = "") {
64
+ if (obj == null || typeof obj !== "object") {
65
+ const marker = isNonDefault(obj, defaults, prefix) ? " *" : ""
66
+ console.log(`${indent}${formatValue(obj)}${marker}`)
67
+ return
68
+ }
69
+ for (const [key, value] of Object.entries(obj)) {
70
+ const fullPath = prefix ? `${prefix}.${key}` : key
71
+ if (value != null && typeof value === "object" && !Array.isArray(value)) {
72
+ console.log(`${indent}${key}:`)
73
+ printTree(value, defaults, fullPath, indent + " ")
74
+ } else {
75
+ const marker = isNonDefault(value, getByPath(defaults, fullPath), "") ? " *" : ""
76
+ console.log(`${indent}${key}: ${formatValue(value)}${marker}`)
77
+ }
78
+ }
79
+ }
80
+
81
+ function formatValue(v) {
82
+ if (v === null) return "null"
83
+ if (Array.isArray(v)) return JSON.stringify(v)
84
+ return String(v)
85
+ }
86
+
18
87
  export function createConfigCommand() {
19
88
  const cmd = new Command("config").description("manage kkcode config")
20
89
 
@@ -40,5 +109,83 @@ export function createConfigCommand() {
40
109
  console.log(`imported config written: ${to}`)
41
110
  })
42
111
 
112
+ cmd
113
+ .command("show")
114
+ .description("show current effective config (merged)")
115
+ .option("--section <name>", "show only a specific top-level section")
116
+ .action(async (options) => {
117
+ const { config, source } = await loadConfig(process.cwd())
118
+ console.log("# effective config (user + project + defaults)")
119
+ if (source.userPath) console.log(`# user: ${source.userPath}`)
120
+ if (source.projectPath) console.log(`# project: ${source.projectPath}`)
121
+ console.log("# entries marked with * differ from defaults\n")
122
+ const target = options.section ? { [options.section]: config[options.section] } : config
123
+ if (options.section && config[options.section] === undefined) {
124
+ console.error(`unknown section: ${options.section}`)
125
+ process.exitCode = 1
126
+ return
127
+ }
128
+ printTree(target, DEFAULT_CONFIG)
129
+ })
130
+
131
+ cmd
132
+ .command("get <key>")
133
+ .description("get a config value by dot-path (e.g. provider.default)")
134
+ .action(async (key) => {
135
+ const { config } = await loadConfig(process.cwd())
136
+ const value = getByPath(config, key)
137
+ if (value === undefined) {
138
+ console.error(`key not found: ${key}`)
139
+ process.exitCode = 1
140
+ return
141
+ }
142
+ if (value != null && typeof value === "object") {
143
+ console.log(YAML.stringify(value).trimEnd())
144
+ } else {
145
+ console.log(formatValue(value))
146
+ }
147
+ })
148
+
149
+ cmd
150
+ .command("set <key> <value>")
151
+ .description("set a config value in project config (e.g. provider.default anthropic)")
152
+ .action(async (key, rawValue) => {
153
+ const cwd = process.cwd()
154
+ const candidates = projectConfigCandidates(cwd)
155
+ let configPath = null
156
+ for (const c of candidates) {
157
+ try {
158
+ await readFile(c, "utf8")
159
+ configPath = c
160
+ break
161
+ } catch { /* not found */ }
162
+ }
163
+ if (!configPath) {
164
+ configPath = candidates[0]
165
+ await mkdir(path.dirname(configPath), { recursive: true })
166
+ }
167
+
168
+ let existing = {}
169
+ try {
170
+ const raw = await readFile(configPath, "utf8")
171
+ existing = parseInput(configPath, raw) || {}
172
+ } catch { /* new file */ }
173
+
174
+ const value = coerceValue(rawValue)
175
+ setByPath(existing, key, value)
176
+
177
+ const check = validateConfig(existing)
178
+ if (!check.valid) {
179
+ console.error("resulting config is invalid:")
180
+ for (const e of check.errors) console.error(` - ${e}`)
181
+ process.exitCode = 1
182
+ return
183
+ }
184
+
185
+ await writeFile(configPath, stringifyOutput(configPath, existing), "utf8")
186
+ console.log(`${key} = ${formatValue(value)}`)
187
+ console.log(`written: ${configPath}`)
188
+ })
189
+
43
190
  return cmd
44
191
  }
@@ -8,6 +8,7 @@ import { auditStats } from "../storage/audit-store.mjs"
8
8
  import { fsckSessionStore, flushNow } from "../session/store.mjs"
9
9
  import { BackgroundManager } from "../orchestration/background-manager.mjs"
10
10
  import { McpRegistry } from "../mcp/registry.mjs"
11
+ import { SkillRegistry } from "../skill/registry.mjs"
11
12
 
12
13
  const exec = promisify(execCb)
13
14
 
@@ -69,8 +70,19 @@ async function buildDoctorReport() {
69
70
  const storage = await fsckSessionStore()
70
71
  const backgroundTasks = await BackgroundManager.list()
71
72
  await McpRegistry.initialize(config)
73
+ await SkillRegistry.initialize({ ...config, skills: { ...(config.skills || {}), auto_seed: false } }, process.cwd())
72
74
  const mcpSnapshot = McpRegistry.healthSnapshot()
73
75
  const mcpHealthy = mcpSnapshot.filter((item) => item.ok).length
76
+ const skillList = SkillRegistry.list()
77
+ const skillSummary = {
78
+ enabled: config.skills?.enabled !== false,
79
+ autoSeed: config.skills?.auto_seed !== false,
80
+ total: skillList.length,
81
+ template: skillList.filter((s) => s.type === "template").length,
82
+ skillMd: skillList.filter((s) => s.type === "skill_md").length,
83
+ mcpPrompt: skillList.filter((s) => s.type === "mcp_prompt").length,
84
+ programmable: skillList.filter((s) => s.type === "mjs").length
85
+ }
74
86
 
75
87
  return {
76
88
  ok: storage.ok,
@@ -94,6 +106,7 @@ async function buildDoctorReport() {
94
106
  unhealthy: mcpSnapshot.length - mcpHealthy,
95
107
  servers: mcpSnapshot
96
108
  },
109
+ skills: skillSummary,
97
110
  storage: {
98
111
  sessions: storage,
99
112
  eventLog: events,
@@ -120,11 +133,18 @@ function printTextReport(report, themeWarnings = []) {
120
133
  }
121
134
  for (const p of report.runtime.providersConfigured) {
122
135
  console.log(
123
- `provider:${p.name} type=${p.type} model=${p.model || "?"} key=${p.apiKeyEnv || "-"} (${p.apiKeyConfigured ? "set" : "missing"})`
136
+ `provider:${p.name} type=${p.type} model=${p.model || "?"} env=${p.apiKeyEnv || "-"} (${p.apiKeyConfigured ? "set" : "missing"})`
124
137
  )
125
138
  }
126
139
  console.log(`check node=${report.checks.node ? "ok" : "missing"} rg=${report.checks.rg ? "ok" : "missing"} git=${report.checks.git ? "ok" : "missing"}`)
127
140
  console.log(`mcp: configured=${report.mcp.configured} healthy=${report.mcp.healthy} unhealthy=${report.mcp.unhealthy}`)
141
+ console.log(`skills: total=${report.skills.total} template=${report.skills.template + report.skills.skillMd} mcp=${report.skills.mcpPrompt} programmable=${report.skills.programmable}`)
142
+ if (report.mcp.configured === 0) {
143
+ console.log(" mcp quickstart: kkcode mcp init --project --with-skills")
144
+ }
145
+ if (report.skills.total === 0) {
146
+ console.log(" skills quickstart: kkcode skill init --project")
147
+ }
128
148
  console.log(`sessions: ok=${report.storage.sessions.ok} index=${report.storage.sessions.sessionsInIndex} files=${report.storage.sessions.filesOnDisk}`)
129
149
  console.log(`events: active=${report.storage.eventLog.activeBytes} rotated=${report.storage.eventLog.rotatedFiles}`)
130
150
  console.log(`audit: total=${report.storage.audit.total} error1h=${report.storage.audit.error1h} error24h=${report.storage.audit.error24h}`)
@@ -138,11 +158,15 @@ export function createDoctorCommand() {
138
158
  .description("run environment diagnostics")
139
159
  .option("--json", "print structured diagnostics", false)
140
160
  .action(async (options) => {
141
- const report = await buildDoctorReport()
142
- if (options.json) {
143
- console.log(JSON.stringify(report, null, 2))
144
- return
161
+ try {
162
+ const report = await buildDoctorReport()
163
+ if (options.json) {
164
+ console.log(JSON.stringify(report, null, 2))
165
+ return
166
+ }
167
+ printTextReport(report, report.themeWarnings || [])
168
+ } finally {
169
+ McpRegistry.shutdown()
145
170
  }
146
- printTextReport(report, report.themeWarnings || [])
147
171
  })
148
172
  }
@@ -35,6 +35,23 @@ const PROVIDER_DEFAULTS = {
35
35
  "openai-compatible": { base_url: "", api_key_env: "", default_model: "" }
36
36
  }
37
37
 
38
+ // Auto-detect available providers by checking environment variables
39
+ function detectProvider() {
40
+ const registered = new Set(listProviders())
41
+ const checks = [
42
+ { provider: "anthropic", env: "ANTHROPIC_API_KEY" },
43
+ { provider: "openai", env: "OPENAI_API_KEY" }
44
+ ]
45
+ const available = []
46
+ for (const { provider, env } of checks) {
47
+ if (process.env[env] && registered.has(provider)) available.push(provider)
48
+ }
49
+ // Prefer anthropic if both are set, otherwise first available, fallback to openai
50
+ if (available.includes("anthropic")) return { provider: "anthropic", detected: available }
51
+ if (available.length) return { provider: available[0], detected: available }
52
+ return { provider: "openai", detected: [] }
53
+ }
54
+
38
55
  function buildConfig(answers) {
39
56
  const config = {
40
57
  provider: {
@@ -56,7 +73,11 @@ function buildConfig(answers) {
56
73
 
57
74
  async function runInteractive(rl) {
58
75
  const providers = listProviders()
59
- const provider = await askChoice(rl, "select default provider:", providers, "openai")
76
+ const { provider: detectedProvider, detected } = detectProvider()
77
+ if (detected.length) {
78
+ console.log(`\nauto-detected API keys: ${detected.join(", ")}`)
79
+ }
80
+ const provider = await askChoice(rl, "select default provider:", providers, detectedProvider)
60
81
  const defaults = PROVIDER_DEFAULTS[provider] || {}
61
82
 
62
83
  let baseUrl = ""
@@ -102,11 +123,16 @@ export function createInitCommand() {
102
123
 
103
124
  let answers
104
125
  if (options.yes) {
126
+ const { provider, detected } = detectProvider()
127
+ const defaults = PROVIDER_DEFAULTS[provider] || {}
128
+ if (detected.length) {
129
+ console.log(`auto-detected: ${detected.join(", ")} → using ${provider}`)
130
+ }
105
131
  answers = {
106
- provider: "openai",
107
- baseUrl: "",
108
- apiKeyEnv: "OPENAI_API_KEY",
109
- model: "gpt-5.3-codex",
132
+ provider,
133
+ baseUrl: defaults.base_url || "",
134
+ apiKeyEnv: defaults.api_key_env || "",
135
+ model: defaults.default_model || "",
110
136
  permissionPolicy: "ask"
111
137
  }
112
138
  } else {
@@ -134,7 +160,7 @@ export function createInitCommand() {
134
160
  const defaultProvider = config.provider.default
135
161
  const providerCfg = config.provider[defaultProvider] || {}
136
162
  if (providerCfg.api_key_env && !process.env[providerCfg.api_key_env]) {
137
- console.log(`note: ${providerCfg.api_key_env} is not set in environment`)
163
+ console.log(`note: environment variable ${providerCfg.api_key_env} is not set`)
138
164
  }
139
165
  console.log("run 'kkcode doctor' for full diagnostics")
140
166
  })
@@ -1,5 +1,8 @@
1
1
  import { Command } from "commander"
2
+ import { readFile } from "node:fs/promises"
2
3
  import { LongAgentManager } from "../orchestration/longagent-manager.mjs"
4
+ import { loadConfig } from "../config/load-config.mjs"
5
+ import { eventLogPath } from "../storage/paths.mjs"
3
6
 
4
7
  export function createLongagentCommand() {
5
8
  const cmd = new Command("longagent").description("manage longagent sessions")
@@ -96,5 +99,119 @@ export function createLongagentCommand() {
96
99
  console.log(`stage retry requested: ${options.stage} (session=${out.sessionId})`)
97
100
  })
98
101
 
102
+ cmd
103
+ .command("logs")
104
+ .description("view longagent event logs")
105
+ .option("--session <id>", "filter by session id")
106
+ .option("-n, --lines <n>", "number of recent lines", "50")
107
+ .option("--json", "output raw JSON lines")
108
+ .action(async (options) => {
109
+ const logFile = eventLogPath()
110
+ let raw
111
+ try {
112
+ raw = await readFile(logFile, "utf8")
113
+ } catch {
114
+ console.error(`no event log found at ${logFile}`)
115
+ process.exitCode = 1
116
+ return
117
+ }
118
+ const allLines = raw.trim().split("\n").filter(Boolean)
119
+ let events = allLines.map((line) => {
120
+ try { return JSON.parse(line) } catch { return null }
121
+ }).filter(Boolean)
122
+
123
+ if (options.session) {
124
+ events = events.filter((e) => e.sessionId === options.session)
125
+ }
126
+ // filter longagent-related events
127
+ events = events.filter((e) =>
128
+ String(e.type || "").includes("longagent") ||
129
+ String(e.type || "").includes("stage") ||
130
+ String(e.type || "").includes("task")
131
+ )
132
+ const limit = Math.max(1, Number(options.lines) || 50)
133
+ events = events.slice(-limit)
134
+
135
+ if (options.json) {
136
+ for (const e of events) console.log(JSON.stringify(e))
137
+ return
138
+ }
139
+ if (!events.length) {
140
+ console.log("no longagent events found")
141
+ return
142
+ }
143
+ for (const e of events) {
144
+ const ts = e.timestamp ? new Date(e.timestamp).toISOString().slice(11, 19) : "??:??:??"
145
+ const sid = e.sessionId ? e.sessionId.slice(0, 12) : "????????????"
146
+ const payload = e.payload ? JSON.stringify(e.payload).slice(0, 120) : ""
147
+ console.log(`${ts} [${sid}] ${e.type || "unknown"}${payload ? " " + payload : ""}`)
148
+ }
149
+ })
150
+
151
+ cmd
152
+ .command("config")
153
+ .description("show effective longagent configuration")
154
+ .option("--full", "show full merged config (not just longagent section)")
155
+ .action(async (options) => {
156
+ const configState = await loadConfig()
157
+ if (options.full) {
158
+ console.log(JSON.stringify(configState.config, null, 2))
159
+ return
160
+ }
161
+ const la = configState.config.agent?.longagent || {}
162
+ console.log("## longagent config")
163
+ console.log(JSON.stringify(la, null, 2))
164
+ console.log("\n## sources")
165
+ console.log(` user: ${configState.source.userPath || "(none)"}`)
166
+ console.log(` project: ${configState.source.projectPath || "(none)"}`)
167
+ console.log(` env: ${configState.source.envPath || "(none)"}`)
168
+ if (configState.errors.length) {
169
+ console.log("\n## errors")
170
+ for (const e of configState.errors) console.log(` - ${e}`)
171
+ }
172
+ })
173
+
174
+ cmd
175
+ .command("start")
176
+ .description("launch a longagent session with a prompt")
177
+ .argument("<prompt>", "task description for longagent")
178
+ .option("--model <model>", "override model")
179
+ .option("--provider <type>", "override provider type")
180
+ .option("--max-iterations <n>", "max iterations (0=unlimited)", "0")
181
+ .action(async (prompt, options) => {
182
+ const configState = await loadConfig()
183
+ const providerKey = options.provider || configState.config.provider.default
184
+ const providerConf = configState.config.provider[providerKey] || {}
185
+ const model = options.model || providerConf.default_model
186
+ if (!model) {
187
+ console.error(`no model configured for provider "${providerKey}"`)
188
+ process.exitCode = 1
189
+ return
190
+ }
191
+ const { executeTurn } = await import("../session/engine.mjs")
192
+ const { newSessionId } = await import("../session/engine.mjs")
193
+ const sessionId = newSessionId()
194
+ console.log(`starting longagent session: ${sessionId}`)
195
+ console.log(`model: ${model}, provider: ${providerKey}`)
196
+ try {
197
+ const result = await executeTurn({
198
+ prompt,
199
+ mode: "longagent",
200
+ model,
201
+ sessionId,
202
+ configState: { config: configState.config, source: configState.source },
203
+ providerType: providerKey,
204
+ baseUrl: providerConf.base_url || null,
205
+ apiKeyEnv: providerConf.api_key_env || null,
206
+ maxIterations: Number(options.maxIterations) || 0,
207
+ output: { write: (t) => process.stdout.write(t) }
208
+ })
209
+ console.log(`\nsession ${sessionId} finished (status: ${result.status || "done"})`)
210
+ } catch (err) {
211
+ console.error(`longagent error: ${err.message}`)
212
+ process.exitCode = 1
213
+ }
214
+ })
215
+
99
216
  return cmd
100
217
  }