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