@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.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 (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  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 +89 -89
  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/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -0,0 +1,208 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises"
2
+ import { dirname } from "node:path"
3
+ import { paint } from "../theme/color.mjs"
4
+ import { resolveMode } from "../session/engine.mjs"
5
+
6
+ export function configuredProviders(config, listProvidersFn) {
7
+ const builtins = new Set(listProvidersFn())
8
+ const out = []
9
+ for (const [name, value] of Object.entries(config.provider || {})) {
10
+ if (name === "default") continue
11
+ if (name === "strict_mode") continue
12
+ if (name === "model_context") continue
13
+ if (!value || typeof value !== "object") continue
14
+ const type = value.type || name
15
+ if (builtins.has(type)) out.push(name)
16
+ }
17
+ return out
18
+ }
19
+
20
+ export async function loadHistoryLines(filePath, size) {
21
+ try {
22
+ const raw = await readFile(filePath, "utf8")
23
+ return raw.split("\n").filter(Boolean).slice(-size)
24
+ } catch {
25
+ return []
26
+ }
27
+ }
28
+
29
+ export async function saveHistoryLines(filePath, size, lines) {
30
+ try {
31
+ await mkdir(dirname(filePath), { recursive: true })
32
+ const finalLines = [...lines].slice(-size)
33
+ await writeFile(filePath, finalLines.join("\n") + (finalLines.length ? "\n" : ""), "utf8")
34
+ } catch {}
35
+ }
36
+
37
+ export function clearScreen(output = process.stdout) {
38
+ if (!output?.isTTY) return
39
+ output.write("\x1Bc")
40
+ }
41
+
42
+ export function resolveProviderDefaultModel(config, providerType, fallback = "") {
43
+ return (
44
+ config.provider?.[providerType]?.default_model ||
45
+ config.provider?.[config.provider?.default]?.default_model ||
46
+ fallback
47
+ )
48
+ }
49
+
50
+ export function createInitialReplState(config, { newSessionIdFn }) {
51
+ const providerType = config.provider.default
52
+ const state = {
53
+ sessionId: newSessionIdFn(),
54
+ mode: resolveMode(config.agent.default_mode),
55
+ providerType,
56
+ model: ""
57
+ }
58
+ state.model = resolveProviderDefaultModel(config, providerType)
59
+ return state
60
+ }
61
+
62
+ export function collectMcpStatusLines(theme, entries, tools) {
63
+ const lines = []
64
+ for (const entry of entries) {
65
+ if (entry.ok) {
66
+ const toolCount = tools.filter((tool) => tool.server === entry.name).length
67
+ lines.push(
68
+ paint(` mcp ✓ ${entry.name}`, theme.semantic.success) +
69
+ paint(` (${toolCount} tools, ${entry.transport})`, theme.base.muted)
70
+ )
71
+ continue
72
+ }
73
+ const reason = entry.error || entry.reason || "unknown"
74
+ lines.push(
75
+ paint(` mcp ✗ ${entry.name}`, theme.semantic.error) +
76
+ paint(` ${reason}`, theme.base.muted)
77
+ )
78
+ }
79
+ return lines
80
+ }
81
+
82
+ export function startSplash({
83
+ paintFn = paint,
84
+ stdout = process.stdout,
85
+ version = "v0.1.27"
86
+ } = {}) {
87
+ if (!stdout?.isTTY) return { update() {}, stop() {} }
88
+
89
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
90
+ const logo = [
91
+ " ██╗ ██╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗ ",
92
+ " ██║ ██╔╝ ██║ ██╔╝ ██╔════╝ ██╔═══██╗ ██╔══██╗ ██╔════╝ ",
93
+ " █████╔╝ █████╔╝ ██║ ██║ ██║ ██║ ██║ █████╗ ",
94
+ " ██╔═██╗ ██╔═██╗ ██║ ██║ ██║ ██║ ██║ ██╔══╝ ",
95
+ " ██║ ██╗ ██║ ██╗ ╚██████╗ ╚██████╔╝ ██████╔╝ ███████╗ ",
96
+ " ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ "
97
+ ]
98
+ const tagline = "CLI Personal Assistant"
99
+ const wave = [
100
+ "#4af5f0", "#3de8f5", "#30dbfa", "#38c8ff", "#40b5ff",
101
+ "#58a0ff", "#708bff", "#8876ff", "#a061ff", "#b84cff",
102
+ "#d037ff", "#e828f0", "#f034d0", "#f040b0", "#f04c90",
103
+ "#f040b0", "#f034d0", "#e828f0", "#d037ff", "#b84cff",
104
+ "#a061ff", "#8876ff", "#708bff", "#58a0ff", "#40b5ff",
105
+ "#38c8ff", "#30dbfa", "#3de8f5"
106
+ ]
107
+
108
+ function charColor(ch, hex) {
109
+ if (ch === " " || ch === "\n") return ch
110
+ const r = parseInt(hex.slice(1, 3), 16)
111
+ const g = parseInt(hex.slice(3, 5), 16)
112
+ const b = parseInt(hex.slice(5, 7), 16)
113
+ return `\x1b[1;38;2;${r};${g};${b}m${ch}\x1b[0m`
114
+ }
115
+
116
+ let tick = 0
117
+ let status = "loading config..."
118
+ let steps = []
119
+ let revealChars = 0
120
+ const totalChars = logo[0].length
121
+ const revealSpeed = 3
122
+
123
+ function render() {
124
+ const cols = stdout.columns || 80
125
+ const rows = stdout.rows || 24
126
+ const lines = []
127
+ const contentHeight = logo.length + 4 + steps.length + 2
128
+ const topPad = Math.max(0, Math.floor((rows - contentHeight) / 2))
129
+ for (let i = 0; i < topPad; i++) lines.push("")
130
+
131
+ const visible = Math.min(revealChars, totalChars)
132
+ for (let row = 0; row < logo.length; row++) {
133
+ const line = logo[row]
134
+ const pad = Math.max(0, Math.floor((cols - line.length) / 2))
135
+ let out = " ".repeat(pad)
136
+ for (let col = 0; col < line.length; col++) {
137
+ if (col >= visible) {
138
+ out += " "
139
+ continue
140
+ }
141
+ const ch = line[col]
142
+ const waveIdx = (col + tick * 2 + row * 3) % wave.length
143
+ out += charColor(ch, wave[waveIdx])
144
+ }
145
+ lines.push(out)
146
+ }
147
+
148
+ const tagFull = `${tagline} · ${version}`
149
+ if (visible >= totalChars) {
150
+ const tagPad = Math.max(0, Math.floor((cols - tagFull.length) / 2))
151
+ const tagAlpha = Math.min(1, (revealChars - totalChars) / 20)
152
+ const brightness = Math.round(100 + 155 * tagAlpha)
153
+ const hex = brightness.toString(16).padStart(2, "0")
154
+ const tagHex = `#${hex}${hex}${hex}`
155
+ lines.push(" ".repeat(tagPad) + paintFn(tagFull, tagHex, { dim: tagAlpha < 0.5 }))
156
+ } else {
157
+ lines.push("")
158
+ }
159
+
160
+ if (visible >= totalChars) {
161
+ const barWidth = Math.min(40, cols - 4)
162
+ const barPad = Math.max(0, Math.floor((cols - barWidth) / 2))
163
+ let bar = ""
164
+ for (let i = 0; i < barWidth; i++) {
165
+ const ci = (i + tick) % wave.length
166
+ bar += charColor("─", wave[ci])
167
+ }
168
+ lines.push(" ".repeat(barPad) + bar)
169
+ } else {
170
+ lines.push("")
171
+ }
172
+
173
+ lines.push("")
174
+ for (const step of steps) {
175
+ const pad = Math.max(0, Math.floor((cols - step.length - 4) / 2))
176
+ lines.push(" ".repeat(pad) + paintFn(` ✓ ${step}`, "#3fd487"))
177
+ }
178
+
179
+ const spinChar = frames[tick % frames.length]
180
+ const spinLine = `${spinChar} ${status}`
181
+ const spinPad = Math.max(0, Math.floor((cols - spinLine.length - 2) / 2))
182
+ lines.push(" ".repeat(spinPad) + paintFn(` ${spinLine}`, "#6ec1ff", { bold: true }))
183
+
184
+ stdout.write("\x1B[?25l")
185
+ stdout.write("\x1Bc")
186
+ stdout.write(lines.join("\n"))
187
+ }
188
+
189
+ render()
190
+ const timer = setInterval(() => {
191
+ tick += 1
192
+ if (revealChars < totalChars + 30) revealChars += revealSpeed
193
+ render()
194
+ }, 50)
195
+
196
+ return {
197
+ update(text) {
198
+ steps.push(status.replace("...", ""))
199
+ status = text
200
+ render()
201
+ },
202
+ stop() {
203
+ clearInterval(timer)
204
+ stdout.write("\x1B[?25h")
205
+ stdout.write("\x1Bc")
206
+ }
207
+ }
208
+ }
@@ -0,0 +1,87 @@
1
+ export const QUESTION_SKIPPED = "(skipped)"
2
+
3
+ export function activateNextQuestionState(queue = []) {
4
+ if (!queue.length) {
5
+ return {
6
+ pendingQuestion: null,
7
+ questionIndex: 0,
8
+ questionOptionSelected: 0,
9
+ questionMultiSelected: {},
10
+ questionCustomMode: false,
11
+ questionCustomInput: "",
12
+ questionCustomCursor: 0,
13
+ questionAnswers: {}
14
+ }
15
+ }
16
+
17
+ const [pendingQuestion, ...rest] = queue
18
+ return {
19
+ queue: rest,
20
+ pendingQuestion,
21
+ questionIndex: 0,
22
+ questionOptionSelected: 0,
23
+ questionMultiSelected: {},
24
+ questionCustomMode: false,
25
+ questionCustomInput: "",
26
+ questionCustomCursor: 0,
27
+ questionAnswers: {}
28
+ }
29
+ }
30
+
31
+ export function commitQuestionAnswer(state) {
32
+ const questions = state.pendingQuestion?.questions || []
33
+ const current = questions[state.questionIndex]
34
+ if (!current) return state
35
+
36
+ const nextAnswers = { ...state.questionAnswers }
37
+ if (state.questionCustomMode) {
38
+ nextAnswers[current.id] = state.questionCustomInput || ""
39
+ return {
40
+ ...state,
41
+ questionAnswers: nextAnswers,
42
+ questionCustomMode: false,
43
+ questionCustomInput: "",
44
+ questionCustomCursor: 0
45
+ }
46
+ }
47
+
48
+ if (current.multi) {
49
+ const selected = state.questionMultiSelected[current.id] || new Set()
50
+ const values = [...selected]
51
+ .map((index) => {
52
+ const opt = (current.options || [])[index]
53
+ return opt ? (opt.value || opt.label) : ""
54
+ })
55
+ .filter(Boolean)
56
+ nextAnswers[current.id] = values.join(", ")
57
+ return { ...state, questionAnswers: nextAnswers }
58
+ }
59
+
60
+ const option = (current.options || [])[state.questionOptionSelected]
61
+ if (option) nextAnswers[current.id] = option.value || option.label
62
+ return { ...state, questionAnswers: nextAnswers }
63
+ }
64
+
65
+ export function advanceQuestionState(state) {
66
+ const questions = state.pendingQuestion?.questions || []
67
+ if (state.questionIndex < questions.length - 1) {
68
+ return {
69
+ ...state,
70
+ questionIndex: state.questionIndex + 1,
71
+ questionOptionSelected: 0,
72
+ questionCustomMode: false,
73
+ questionCustomInput: "",
74
+ questionCustomCursor: 0
75
+ }
76
+ }
77
+ return { ...state, shouldSubmit: true }
78
+ }
79
+
80
+ export function finalizeQuestionAnswers(pendingQuestion, questionAnswers = {}) {
81
+ const answers = { ...questionAnswers }
82
+ const questions = pendingQuestion?.questions || []
83
+ for (const question of questions) {
84
+ if (!(question.id in answers)) answers[question.id] = QUESTION_SKIPPED
85
+ }
86
+ return answers
87
+ }
@@ -0,0 +1,76 @@
1
+ export async function collectInput(rl, promptStr) {
2
+ const first = (await rl.question(promptStr)).trim()
3
+ if (!first) return ""
4
+
5
+ if (first === '"""' || first.startsWith('"""')) {
6
+ const lines = []
7
+ if (first !== '"""') lines.push(first.slice(3))
8
+ while (true) {
9
+ const next = await rl.question("... ")
10
+ if (next.trim() === '"""') break
11
+ lines.push(next)
12
+ }
13
+ return lines.join("\n").trim()
14
+ }
15
+
16
+ if (first.endsWith("\\")) {
17
+ const lines = [first.slice(0, -1)]
18
+ while (true) {
19
+ const next = await rl.question("... ")
20
+ if (next.endsWith("\\")) lines.push(next.slice(0, -1))
21
+ else {
22
+ lines.push(next)
23
+ break
24
+ }
25
+ }
26
+ return lines.join("\n").trim()
27
+ }
28
+
29
+ return first
30
+ }
31
+
32
+ export function resolveHistoryNavigation(history, historyIndex, keyName) {
33
+ if (!Array.isArray(history) || history.length === 0) {
34
+ return { historyIndex, value: "", changed: false }
35
+ }
36
+
37
+ if (keyName === "up") {
38
+ const nextIndex = Math.max(0, historyIndex - 1)
39
+ return {
40
+ historyIndex: nextIndex,
41
+ value: history[nextIndex] || "",
42
+ changed: nextIndex !== historyIndex
43
+ }
44
+ }
45
+
46
+ if (historyIndex < history.length - 1) {
47
+ const nextIndex = historyIndex + 1
48
+ return {
49
+ historyIndex: nextIndex,
50
+ value: history[nextIndex] || "",
51
+ changed: true
52
+ }
53
+ }
54
+
55
+ return {
56
+ historyIndex: history.length,
57
+ value: "",
58
+ changed: historyIndex !== history.length
59
+ }
60
+ }
61
+
62
+ export function shouldApplySuggestionOnEnter(input, suggestions, selectedSuggestion) {
63
+ if (!Array.isArray(suggestions) || suggestions.length === 0) return false
64
+ if (!String(input || "").startsWith("/")) return false
65
+
66
+ const body = String(input || "").slice(1)
67
+ const firstSpace = body.indexOf(" ")
68
+ if (firstSpace >= 0) return false
69
+
70
+ const token = body.trim()
71
+ if (!token) return true
72
+
73
+ const idx = Math.max(0, Math.min(selectedSuggestion || 0, suggestions.length - 1))
74
+ const chosen = suggestions[idx]
75
+ return Boolean(chosen && chosen.name !== token)
76
+ }
@@ -0,0 +1,7 @@
1
+ export const MODE_CYCLE_ORDER = ["assistant", "agent", "longagent", "plan"]
2
+
3
+ export function nextMode(currentMode, order = MODE_CYCLE_ORDER) {
4
+ const idx = order.indexOf(currentMode)
5
+ const nextIdx = idx >= 0 ? (idx + 1) % order.length : 0
6
+ return order[nextIdx]
7
+ }
@@ -0,0 +1,15 @@
1
+ export function buildOperatorSnapshot({ runtimeSummary = null, backgroundSummary = null } = {}) {
2
+ const actions = []
3
+ if (runtimeSummary?.recoverableCount) {
4
+ actions.push(`recoverable sessions available: ${runtimeSummary.recoverableCount}`)
5
+ }
6
+ const recentTerminal = backgroundSummary?.recent_terminal || []
7
+ for (const item of recentTerminal.slice(0, 2)) {
8
+ if (item?.next_action) actions.push(`${item.id}: ${item.next_action}`)
9
+ }
10
+ return {
11
+ recoverableCount: Number(runtimeSummary?.recoverableCount || 0),
12
+ activeBackground: Number(backgroundSummary?.active || 0),
13
+ actions
14
+ }
15
+ }
@@ -0,0 +1,49 @@
1
+ export const POLICY_CHOICES = [
2
+ { label: "Auto", value: "auto", desc: "auto-approve safe reads, review risky tools" },
3
+ { label: "YOLO", value: "yolo", desc: "allow permission checks without prompts" },
4
+ { label: "Ask", value: "ask", desc: "prompt before each tool call" },
5
+ { label: "Allow", value: "allow", desc: "legacy allow all tool calls" },
6
+ { label: "Deny", value: "deny", desc: "deny all tool calls" },
7
+ { label: "Session Clear", value: "session-clear", desc: "clear cached grants" }
8
+ ]
9
+
10
+ function currentPermissionValue(permissionConfigOrValue = "auto") {
11
+ if (typeof permissionConfigOrValue === "string") return permissionConfigOrValue
12
+ return permissionConfigOrValue.mode || permissionConfigOrValue.default_policy || "auto"
13
+ }
14
+
15
+ export function createPolicyPickerState(current = "auto") {
16
+ const value = currentPermissionValue(current)
17
+ const idx = POLICY_CHOICES.findIndex((choice) => choice.value === value)
18
+ return { selected: Math.max(0, idx) }
19
+ }
20
+
21
+ export function applyPolicyChoice(choice, { permissionConfig = {}, sessionId, clearSession } = {}) {
22
+ if (!choice) return { message: null, permissionConfig }
23
+ if (choice.value === "session-clear") {
24
+ clearSession?.(sessionId)
25
+ return {
26
+ message: "permission session cache cleared",
27
+ permissionConfig
28
+ }
29
+ }
30
+
31
+ if (["auto", "yolo"].includes(choice.value)) {
32
+ return {
33
+ message: `permission mode → ${choice.value}`,
34
+ permissionConfig: {
35
+ ...permissionConfig,
36
+ mode: choice.value
37
+ }
38
+ }
39
+ }
40
+
41
+ return {
42
+ message: `permission policy → ${choice.value}`,
43
+ permissionConfig: {
44
+ ...permissionConfig,
45
+ mode: "manual",
46
+ default_policy: choice.value
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,36 @@
1
+ import { listSessions } from "../session/store.mjs"
2
+ import { BackgroundManager } from "../orchestration/background-manager.mjs"
3
+ import { summarizeSessionRuntimeState } from "../session/runtime-state.mjs"
4
+ import { collectMcpSummary, collectSkillSummary } from "./state-store.mjs"
5
+
6
+ export async function buildReplRuntimeSnapshot({
7
+ cwd = process.cwd(),
8
+ state,
9
+ customCommands = [],
10
+ providers = [],
11
+ mcpRegistry,
12
+ skillRegistry,
13
+ recoveryEnabled = true
14
+ }) {
15
+ const recentSessions = await listSessions({ cwd, limit: 6, includeChildren: false }).catch(() => [])
16
+ const mcpSummary = collectMcpSummary(mcpRegistry)
17
+ const skillSummary = collectSkillSummary(skillRegistry)
18
+ const backgroundSummary = await BackgroundManager.summary().catch(() => null)
19
+ const runtimeSummary = await summarizeSessionRuntimeState({
20
+ sessionId: state?.sessionId || null,
21
+ cwd,
22
+ recoveryEnabled
23
+ }).catch(() => null)
24
+
25
+ return {
26
+ state,
27
+ providers,
28
+ recentSessions,
29
+ mcpSummary,
30
+ skillSummary,
31
+ backgroundSummary,
32
+ runtimeSummary,
33
+ customCommandCount: customCommands.length,
34
+ cwd
35
+ }
36
+ }
@@ -0,0 +1,62 @@
1
+ export const DEFAULT_SLASH_ALIASES = {
2
+ "/h": "/help",
3
+ "/?": "/help",
4
+ "/n": "/new",
5
+ "/s": "/session",
6
+ "/k": "/keys",
7
+ "/r": "/resume",
8
+ "/m": "/mode",
9
+ "/p": "/provider",
10
+ "/q": "/exit"
11
+ }
12
+
13
+ export function buildSlashCatalog({ builtinSlash = [], customCommands = [], skills = [] } = {}) {
14
+ const custom = customCommands.map((cmd) => ({
15
+ name: cmd.name,
16
+ desc: `custom (${cmd.scope || "project"})`
17
+ }))
18
+ const customNames = new Set(custom.map((item) => item.name))
19
+ const skillEntries = skills
20
+ .filter((skill) => !customNames.has(skill.name))
21
+ .map((skill) => ({ name: skill.name, desc: `skill (${skill.type})` }))
22
+ return [...builtinSlash, ...custom, ...skillEntries]
23
+ }
24
+
25
+ export function slashQuery(inputLine) {
26
+ if (!String(inputLine || "").startsWith("/")) return null
27
+ const raw = String(inputLine).slice(1)
28
+ const firstSpace = raw.indexOf(" ")
29
+ return (firstSpace >= 0 ? raw.slice(0, firstSpace) : raw).trim()
30
+ }
31
+
32
+ export function slashSuggestions(inputLine, options = {}) {
33
+ const token = slashQuery(inputLine)
34
+ if (token === null) return []
35
+ const all = buildSlashCatalog(options)
36
+ const q = token.toLowerCase()
37
+ return all
38
+ .map((item) => {
39
+ const name = item.name.toLowerCase()
40
+ let rank = 99
41
+ if (!q) rank = 0
42
+ else if (name === q) rank = 0
43
+ else if (name.startsWith(q)) rank = 1
44
+ else if (name.includes(q)) rank = 2
45
+ return { ...item, rank }
46
+ })
47
+ .filter((item) => item.rank < 99)
48
+ .sort((a, b) => (a.rank - b.rank) || a.name.localeCompare(b.name))
49
+ }
50
+
51
+ export function applySuggestionToInput(current, suggestionName) {
52
+ const raw = String(current || "")
53
+ if (!raw.startsWith("/")) return raw
54
+ const body = raw.slice(1)
55
+ const firstSpace = body.indexOf(" ")
56
+ if (firstSpace < 0) return `/${suggestionName} `
57
+ return `/${suggestionName}${body.slice(firstSpace)}`
58
+ }
59
+
60
+ export function normalizeSlashAlias(line, aliases = DEFAULT_SLASH_ALIASES) {
61
+ return aliases[String(line || "")] || line
62
+ }
@@ -0,0 +1,29 @@
1
+ export function collectMcpSummary(registry) {
2
+ const snapshot = registry.healthSnapshot()
3
+ const tools = registry.listTools()
4
+ const byServer = {}
5
+ for (const tool of tools) {
6
+ const server = tool.server || "unknown"
7
+ byServer[server] = (byServer[server] || 0) + 1
8
+ }
9
+ const healthy = snapshot.filter((item) => item.ok).length
10
+ return {
11
+ configured: snapshot.length,
12
+ healthy,
13
+ unhealthy: snapshot.length - healthy,
14
+ tools: tools.length,
15
+ byServer,
16
+ entries: snapshot
17
+ }
18
+ }
19
+
20
+ export function collectSkillSummary(registry) {
21
+ const list = registry.isReady() ? registry.list() : []
22
+ return {
23
+ total: list.length,
24
+ template: list.filter((s) => s.type === "template").length,
25
+ skillMd: list.filter((s) => s.type === "skill_md").length,
26
+ mcpPrompt: list.filter((s) => s.type === "mcp_prompt").length,
27
+ programmable: list.filter((s) => s.type === "mjs").length
28
+ }
29
+ }
@@ -0,0 +1,58 @@
1
+ import { executeTurn } from "../session/engine.mjs"
2
+ import { HookBus } from "../plugin/hook-bus.mjs"
3
+ import { extractImageRefs, buildContentBlocks } from "../tool/image-util.mjs"
4
+
5
+ export async function executePromptTurn({
6
+ prompt,
7
+ state,
8
+ ctx,
9
+ streamSink = null,
10
+ pendingImages = [],
11
+ signal = null,
12
+ deps = {}
13
+ }) {
14
+ const extractImageRefsFn = deps.extractImageRefs || extractImageRefs
15
+ const buildContentBlocksFn = deps.buildContentBlocks || buildContentBlocks
16
+ const chatParamsFn = deps.chatParams || HookBus.chatParams.bind(HookBus)
17
+ const executeTurnFn = deps.executeTurn || executeTurn
18
+ const cwd = deps.cwd || process.cwd()
19
+
20
+ const { text: cleanedPrompt, imagePaths, imageUrls = [] } = extractImageRefsFn(prompt, cwd)
21
+ const effectivePrompt = cleanedPrompt ?? prompt
22
+ let contentBlocks = null
23
+
24
+ if (imagePaths.length || imageUrls.length || pendingImages.length) {
25
+ contentBlocks = await buildContentBlocksFn(effectivePrompt, imagePaths, imageUrls)
26
+ if (typeof contentBlocks === "string") {
27
+ contentBlocks = [{ type: "text", text: contentBlocks }]
28
+ }
29
+ for (const img of pendingImages) {
30
+ if (img && img.type === "image") contentBlocks.push(img)
31
+ }
32
+ }
33
+
34
+ const chatParams = await chatParamsFn({
35
+ prompt: effectivePrompt,
36
+ mode: state.mode,
37
+ model: state.model,
38
+ providerType: state.providerType,
39
+ sessionId: state.sessionId
40
+ })
41
+
42
+ return {
43
+ result: await executeTurnFn({
44
+ prompt: chatParams.prompt ?? effectivePrompt,
45
+ contentBlocks,
46
+ mode: chatParams.mode ?? state.mode,
47
+ model: chatParams.model ?? state.model,
48
+ sessionId: state.sessionId,
49
+ configState: ctx.configState,
50
+ providerType: chatParams.providerType ?? state.providerType,
51
+ longagentImpl: state.longagentImpl ?? null,
52
+ signal,
53
+ output: streamSink && typeof streamSink === "function"
54
+ ? { write: streamSink }
55
+ : null
56
+ })
57
+ }
58
+ }
@@ -0,0 +1,23 @@
1
+ export function buildReplSmokeChecklist() {
2
+ return [
3
+ "start repl in tty mode",
4
+ "verify /help and /status output",
5
+ "verify slash suggestion and selection flow",
6
+ "submit one normal prompt turn",
7
+ "exercise one permission or question dialog",
8
+ "inspect background/task visibility",
9
+ "confirm --help and --version CLI outputs"
10
+ ]
11
+ }
12
+
13
+ export function summarizeVerificationResults(results = []) {
14
+ const total = results.length
15
+ const passed = results.filter((item) => item?.ok === true).length
16
+ const failed = total - passed
17
+ return {
18
+ total,
19
+ passed,
20
+ failed,
21
+ ok: failed === 0
22
+ }
23
+ }