@rind-ai/cli 0.4.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.
@@ -0,0 +1,126 @@
1
+ import {
2
+ isReadonlySlashCommand,
3
+ parseGoalCommand,
4
+ steeringCommandText,
5
+ } from "./slash-command-mode.js";
6
+ import {
7
+ helpText,
8
+ slashResultText,
9
+ } from "./rendering.js";
10
+
11
+ export function createCommandController({
12
+ request,
13
+ turn,
14
+ input = {},
15
+ state = {},
16
+ output = {},
17
+ }) {
18
+ async function handle(text) {
19
+ if (text === "?") {
20
+ output.log?.(helpText(state.slashCommands || []));
21
+ return true;
22
+ }
23
+ if (!String(text || "").startsWith("/")) {
24
+ return false;
25
+ }
26
+ const goal = parseGoalCommand(text);
27
+ if (goal) {
28
+ await input.runGoalCommand?.(goal);
29
+ return true;
30
+ }
31
+ const steering = steeringCommandText(text);
32
+ if (steering !== null) {
33
+ turn.submitSteering(steering, text);
34
+ return true;
35
+ }
36
+ await runSlashCommand(text);
37
+ return true;
38
+ }
39
+
40
+ async function runSlashCommand(text) {
41
+ if (isBareModelCommand(text) && input.isTerminal) {
42
+ await input.runModelSelector?.();
43
+ return;
44
+ }
45
+ if (isCompactCommand(text) && input.isTerminal) {
46
+ input.startCompactCommand?.();
47
+ return;
48
+ }
49
+ if (isBareSessionsCommand(text) && input.isTerminal) {
50
+ await input.runSessionsSelector?.();
51
+ return;
52
+ }
53
+ if (isReadonlySlashCommand(text) && input.isTerminal) {
54
+ input.startReadonlySlashCommand?.(text);
55
+ return;
56
+ }
57
+ const result = await request("slash.execute", { input: text });
58
+ await applyResult(result);
59
+ }
60
+
61
+ async function applyResult(result = {}) {
62
+ if (result.clear_screen) {
63
+ output.clearScreen?.();
64
+ }
65
+ const text = slashResultText(result, state.slashCommands || []);
66
+ if (text) {
67
+ output.log?.(text);
68
+ }
69
+ if (result.context_usage_reset) {
70
+ output.resetContextUsage?.();
71
+ }
72
+ if (result.input_prefill) {
73
+ output.setInputPrefill?.(result.input_prefill);
74
+ }
75
+ if (result.run_turn_input) {
76
+ turn.submit(result.run_turn_input, {
77
+ transient_system_messages: result.transient_system_messages,
78
+ });
79
+ }
80
+ if (result.should_exit) {
81
+ await output.shutdown?.();
82
+ output.exit?.();
83
+ }
84
+ }
85
+
86
+ function normalizeCommands(commands) {
87
+ if (!Array.isArray(commands)) {
88
+ return [];
89
+ }
90
+ const items = [];
91
+ for (const command of commands) {
92
+ const name = singleWord(command?.name);
93
+ if (!name) {
94
+ continue;
95
+ }
96
+ const description = String(command.description || "").trim();
97
+ items.push({ name, description });
98
+ for (const alias of command.aliases || []) {
99
+ const aliasName = singleWord(alias);
100
+ if (aliasName) {
101
+ items.push({ name: aliasName, description: `alias for /${name}` });
102
+ }
103
+ }
104
+ }
105
+ return items.sort((left, right) => left.name.localeCompare(right.name));
106
+ }
107
+
108
+ return { handle, normalizeCommands, applyResult };
109
+ }
110
+
111
+ function singleWord(value) {
112
+ const text = String(value || "").trim().toLowerCase();
113
+ return text && !/\s/.test(text) ? text : "";
114
+ }
115
+
116
+ function isBareModelCommand(value) {
117
+ return String(value || "").trim().toLowerCase() === "/model";
118
+ }
119
+
120
+ function isBareSessionsCommand(value) {
121
+ return String(value || "").trim().toLowerCase() === "/sessions";
122
+ }
123
+
124
+ function isCompactCommand(value) {
125
+ return String(value || "").trim().toLowerCase() === "/compact";
126
+ }
@@ -0,0 +1,22 @@
1
+ export function createCompactContextState() {
2
+ let awaitingPostCompactContext = false;
3
+ return {
4
+ handleContextBuilt(event = {}) {
5
+ const decisions = event.decisions && typeof event.decisions === "object" ? event.decisions : {};
6
+ if (awaitingPostCompactContext) {
7
+ awaitingPostCompactContext = false;
8
+ return true;
9
+ }
10
+ if (decisions.auto_compact_token_limit_reached || decisions.compact_required) {
11
+ awaitingPostCompactContext = true;
12
+ }
13
+ return false;
14
+ },
15
+ clear() {
16
+ awaitingPostCompactContext = false;
17
+ },
18
+ pending() {
19
+ return awaitingPostCompactContext;
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,203 @@
1
+ import { graphemes, stripAnsi, textWidth, wrapTextCells } from "./text-width.js";
2
+
3
+ const DEFAULT_COLUMNS = 80;
4
+ const INPUT_MARKER = "\n ▷ ";
5
+ const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
6
+ const SGR_SEQUENCE = /^\x1b\[([0-9;]*)m$/;
7
+
8
+ export function prepareComposerFrame(frame = {}, columns = DEFAULT_COLUMNS) {
9
+ const width = terminalColumns(columns);
10
+ const block = splitPromptBlock(frame.prompt);
11
+ const input = String(frame.inputText || "");
12
+ const display = input || String(frame.placeholder || "");
13
+ const position = cursorPosition(frame, input);
14
+ const leadingLines = wrapRenderedLines(block.leading, width);
15
+ const inputLines = wrapInputLines(block.prefix, display, width);
16
+ const cursor = visualCursor(block.prefix, input, position, width, inputLines);
17
+ const menuLines = wrapRenderedLines(frame.menuText, width);
18
+ const trailingLines = wrapRenderedLines(block.trailing, width);
19
+ const lines = [...leadingLines, ...inputLines, ...menuLines, ...trailingLines];
20
+ const cursorRow = leadingLines.length + cursor.row;
21
+ const selectedMenuRow = menuLines.findIndex((line) => stripAnsi(line).startsWith(" › "));
22
+
23
+ return {
24
+ lines,
25
+ cursorRow,
26
+ cursorColumn: cursor.column,
27
+ focusRow: selectedMenuRow === -1
28
+ ? cursorRow
29
+ : leadingLines.length + inputLines.length + selectedMenuRow,
30
+ };
31
+ }
32
+
33
+ function wrapInputLines(prefix, text, columns) {
34
+ const logicalLines = String(text || "").split("\n");
35
+ const prefixWidth = textWidth(prefix);
36
+ const continuationPrefix = " ".repeat(prefixWidth);
37
+ const contentWidth = Math.max(1, columns - prefixWidth);
38
+ const lines = [];
39
+ for (const [index, logicalLine] of logicalLines.entries()) {
40
+ const linePrefix = index === 0 ? prefix : continuationPrefix;
41
+ if (logicalLine.includes("\x1b")) {
42
+ lines.push(...wrapLine(
43
+ linePrefix,
44
+ continuationPrefix,
45
+ logicalLine,
46
+ contentWidth,
47
+ ));
48
+ continue;
49
+ }
50
+ const chunks = wrapTextCells(logicalLine, contentWidth, contentWidth);
51
+ lines.push(...chunks.map((chunk, chunkIndex) => (
52
+ `${chunkIndex === 0 ? linePrefix : continuationPrefix}${chunk.text}`
53
+ )));
54
+ }
55
+ return lines.length ? lines : [String(prefix || "")];
56
+ }
57
+
58
+ function visualCursor(prefix, input, position, columns, inputLines) {
59
+ const logicalLines = String(input).split("\n");
60
+ const prefixWidth = textWidth(prefix);
61
+ const continuationPrefix = " ".repeat(prefixWidth);
62
+ const contentWidth = Math.max(1, columns - prefixWidth);
63
+ const visualLines = [];
64
+ for (const [lineIndex, logicalLine] of logicalLines.entries()) {
65
+ const chunks = wrapTextCells(logicalLine, contentWidth, contentWidth);
66
+ for (const chunk of chunks) {
67
+ visualLines.push({
68
+ startColumn: chunk.startColumn,
69
+ length: chunk.length,
70
+ allowsEnd: chunk.allowsEnd,
71
+ line: lineIndex,
72
+ prefixWidth,
73
+ });
74
+ }
75
+ const lastChunk = chunks.at(-1);
76
+ if (lineIndex === logicalLines.length - 1 && !lastChunk.allowsEnd) {
77
+ visualLines.push({
78
+ startColumn: lastChunk.startColumn + lastChunk.length,
79
+ length: 0,
80
+ allowsEnd: true,
81
+ line: lineIndex,
82
+ prefixWidth,
83
+ });
84
+ }
85
+ }
86
+
87
+ let row = visualLines.length - 1;
88
+ for (const [index, visualLine] of visualLines.entries()) {
89
+ if (visualLine.line !== position.line) {
90
+ continue;
91
+ }
92
+ const offset = position.column - visualLine.startColumn;
93
+ if (offset >= 0 && (offset < visualLine.length || (visualLine.allowsEnd && offset === visualLine.length))) {
94
+ row = index;
95
+ break;
96
+ }
97
+ }
98
+ while (inputLines.length <= row) {
99
+ inputLines.push(continuationPrefix);
100
+ }
101
+ const visualLine = visualLines[row];
102
+ const text = graphemes(logicalLines[position.line])
103
+ .slice(visualLine.startColumn, position.column)
104
+ .join("");
105
+ return {
106
+ row,
107
+ column: visualLine.prefixWidth + textWidth(text),
108
+ };
109
+ }
110
+
111
+ function wrapLine(prefix, continuationPrefix, text, contentWidth) {
112
+ const lines = [];
113
+ let line = "";
114
+ let width = 0;
115
+ let activeStyle = "";
116
+ for (const segment of displaySegments(text)) {
117
+ const sgr = segment.match(SGR_SEQUENCE);
118
+ if (sgr) {
119
+ line += segment;
120
+ const codes = (sgr[1] || "0").split(";").map((code) => code || "0");
121
+ const resetIndex = codes.lastIndexOf("0");
122
+ activeStyle = resetIndex === codes.length - 1
123
+ ? ""
124
+ : resetIndex >= 0
125
+ ? segment
126
+ : `${activeStyle}${segment}`;
127
+ continue;
128
+ }
129
+ const segmentWidth = textWidth(segment);
130
+ if (width > 0 && width + segmentWidth > contentWidth) {
131
+ lines.push(`${lines.length ? continuationPrefix : prefix}${line}`);
132
+ line = activeStyle;
133
+ width = 0;
134
+ }
135
+ line += segment;
136
+ width += segmentWidth;
137
+ }
138
+ lines.push(`${lines.length ? continuationPrefix : prefix}${line}`);
139
+ return lines;
140
+ }
141
+
142
+ function displaySegments(value) {
143
+ const text = String(value || "");
144
+ const segments = [];
145
+ let position = 0;
146
+ for (const match of text.matchAll(ANSI_SEQUENCE)) {
147
+ segments.push(...graphemes(text.slice(position, match.index)), match[0]);
148
+ position = match.index + match[0].length;
149
+ }
150
+ segments.push(...graphemes(text.slice(position)));
151
+ return segments;
152
+ }
153
+
154
+ function cursorPosition(frame, input) {
155
+ const lines = String(input).split("\n");
156
+ const line = Math.min(lines.length - 1, Math.max(0, Math.floor(Number(frame.cursor?.line) || 0)));
157
+ return {
158
+ line,
159
+ column: Math.min(
160
+ graphemes(lines[line]).length,
161
+ Math.max(0, Math.floor(Number(frame.cursor?.column) || 0)),
162
+ ),
163
+ };
164
+ }
165
+
166
+ function terminalColumns(columns) {
167
+ const value = Number(columns);
168
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : DEFAULT_COLUMNS;
169
+ }
170
+
171
+ function splitPromptBlock(prompt) {
172
+ const text = String(prompt || "");
173
+ const index = text.lastIndexOf(INPUT_MARKER);
174
+ if (index === -1) {
175
+ return { leading: "", prefix: text, trailing: "" };
176
+ }
177
+ const inputStart = index + 1;
178
+ const trailingStart = text.indexOf("\n", inputStart);
179
+ if (trailingStart === -1) {
180
+ return {
181
+ leading: text.slice(0, inputStart),
182
+ prefix: text.slice(inputStart),
183
+ trailing: "",
184
+ };
185
+ }
186
+ return {
187
+ leading: text.slice(0, inputStart),
188
+ prefix: text.slice(inputStart, trailingStart),
189
+ trailing: text.slice(trailingStart + 1),
190
+ };
191
+ }
192
+
193
+ function wrapRenderedLines(text, columns) {
194
+ const value = String(text || "");
195
+ if (!value) {
196
+ return [];
197
+ }
198
+ const lines = value.split("\n");
199
+ if (lines.at(-1) === "") {
200
+ lines.pop();
201
+ }
202
+ return lines.flatMap((line) => wrapLine("", "", line, columns));
203
+ }
@@ -0,0 +1,242 @@
1
+ import { runtimeEventType } from "./runtime-protocol.js";
2
+ import {
3
+ cancelledText,
4
+ contextBuiltLine,
5
+ errorLine,
6
+ goalText,
7
+ planUpdatedLine,
8
+ toolProgressLine,
9
+ toolRequestedLine,
10
+ toolResultLine,
11
+ toolStartedLine,
12
+ turnCompletedLine,
13
+ } from "./rendering.js";
14
+
15
+ export function createEventController({
16
+ state = {},
17
+ input = {},
18
+ output = {},
19
+ background = {},
20
+ }) {
21
+ const announcedTools = new Set();
22
+ const pendingFileChanges = new Map();
23
+ const pendingPlanInputs = new Map();
24
+ let toolStats = { completed: 0, failed: 0 };
25
+
26
+ async function handle(message) {
27
+ if (state.runtimeClosing) {
28
+ return;
29
+ }
30
+ const event = message?.event;
31
+ const eventType = runtimeEventType(message);
32
+ if (!event || typeof event !== "object") {
33
+ if (state.debug) {
34
+ output.debug?.(`Ignoring runtime event without payload: ${eventType || "unknown"}`);
35
+ }
36
+ return;
37
+ }
38
+ switch (eventType) {
39
+ case "assistant_delta":
40
+ output.assistantAppend?.(event.text || "");
41
+ return;
42
+ case "context_built": {
43
+ if (output.handleContextBuilt?.(event)) {
44
+ output.resetContextUsage?.();
45
+ }
46
+ const line = contextBuiltLine(event);
47
+ if (line) {
48
+ output.closeAssistant?.();
49
+ output.log?.(line);
50
+ }
51
+ return;
52
+ }
53
+ case "tool_input_started":
54
+ output.closeAssistant?.();
55
+ rememberPlanInputStart(event);
56
+ if (isAnnounced(event)) {
57
+ return;
58
+ }
59
+ output.log?.(toolStartedLine(event));
60
+ return;
61
+ case "tool_input_delta":
62
+ appendPlanInput(event);
63
+ return;
64
+ case "tool_input_ended":
65
+ return;
66
+ case "tool_requested":
67
+ output.closeAssistant?.();
68
+ rememberPlanInputPreview(event);
69
+ background.recordCommand?.(event);
70
+ if (isAnnounced(event)) {
71
+ return;
72
+ }
73
+ output.log?.(toolRequestedLine(event));
74
+ return;
75
+ case "tool_call_started":
76
+ output.closeAssistant?.();
77
+ if (alreadyAnnounced(event)) {
78
+ return;
79
+ }
80
+ output.log?.(toolStartedLine(event));
81
+ return;
82
+ case "tool_result": {
83
+ output.closeAssistant?.();
84
+ const fileChange = pendingFileChanges.get(event.tool_call_id);
85
+ pendingFileChanges.delete(event.tool_call_id);
86
+ const planInput = takePlanInput(event);
87
+ background.recordResult?.(event);
88
+ recordToolResult(event);
89
+ const plan = event.tool_name === "update_plan" && event.status === "completed"
90
+ ? parsePlanInput(planInput)
91
+ : null;
92
+ const goal = event.tool_name === "update_goal" && event.status === "completed"
93
+ ? parseToolData(event.result)
94
+ : null;
95
+ if (goal?.status) {
96
+ output.updateGoal?.(goal);
97
+ }
98
+ output.log?.(goal?.status ? goalText(goal) : plan ? planUpdatedLine(plan) : toolResultLine(event, fileChange));
99
+ return;
100
+ }
101
+ case "file_change":
102
+ if (event.tool_call_id) {
103
+ pendingFileChanges.set(event.tool_call_id, event);
104
+ }
105
+ return;
106
+ case "tool_progress": {
107
+ output.closeAssistant?.();
108
+ const line = toolProgressLine(event);
109
+ if (line) {
110
+ output.log?.(line);
111
+ }
112
+ return;
113
+ }
114
+ case "token_stats_updated":
115
+ output.closeAssistant?.();
116
+ output.setStats?.(event.stats && typeof event.stats === "object" ? event.stats : {});
117
+ if (!state.activeTurn) {
118
+ output.redraw?.();
119
+ }
120
+ return;
121
+ case "user_question_requested":
122
+ await input.answerQuestion?.(event);
123
+ return;
124
+ case "turn_failed":
125
+ output.clearCompactContext?.();
126
+ output.closeAssistant?.();
127
+ output.log?.(errorLine(event.error));
128
+ resetTurnState();
129
+ return;
130
+ case "turn_cancelled":
131
+ output.clearCompactContext?.();
132
+ output.closeAssistant?.();
133
+ output.log?.(cancelledText());
134
+ resetTurnState();
135
+ return;
136
+ case "turn_completed":
137
+ output.clearCompactContext?.();
138
+ output.closeAssistant?.();
139
+ output.log?.(turnCompletedLine(event, toolStats));
140
+ resetTurnState();
141
+ return;
142
+ default:
143
+ if (state.debug) {
144
+ output.debug?.(`Ignoring unknown runtime event: ${eventType || "unknown"}`);
145
+ }
146
+ }
147
+ }
148
+
149
+ function resetTurnState() {
150
+ toolStats = { completed: 0, failed: 0 };
151
+ announcedTools.clear();
152
+ pendingFileChanges.clear();
153
+ pendingPlanInputs.clear();
154
+ output.resetTurnTools?.();
155
+ }
156
+
157
+ function isAnnounced(event) {
158
+ if (!event.tool_call_id) {
159
+ return false;
160
+ }
161
+ if (announcedTools.has(event.tool_call_id)) {
162
+ return true;
163
+ }
164
+ announcedTools.add(event.tool_call_id);
165
+ return false;
166
+ }
167
+
168
+ function alreadyAnnounced(event) {
169
+ return Boolean(event.tool_call_id && announcedTools.has(event.tool_call_id));
170
+ }
171
+
172
+ function recordToolResult(event) {
173
+ if (event.status === "failed") {
174
+ toolStats.failed += 1;
175
+ return;
176
+ }
177
+ toolStats.completed += 1;
178
+ }
179
+
180
+ function rememberPlanInputStart(event) {
181
+ if (event.tool_name === "update_plan" && event.tool_call_id) {
182
+ pendingPlanInputs.set(event.tool_call_id, "");
183
+ }
184
+ }
185
+
186
+ function appendPlanInput(event) {
187
+ if (event.tool_name !== "update_plan" || !event.tool_call_id) {
188
+ return;
189
+ }
190
+ const current = pendingPlanInputs.get(event.tool_call_id);
191
+ if (current !== undefined) {
192
+ pendingPlanInputs.set(event.tool_call_id, current + String(event.delta || ""));
193
+ }
194
+ }
195
+
196
+ function rememberPlanInputPreview(event) {
197
+ if (event.tool_name !== "update_plan" || !event.tool_call_id) {
198
+ return;
199
+ }
200
+ const current = pendingPlanInputs.get(event.tool_call_id);
201
+ if (!current) {
202
+ pendingPlanInputs.set(event.tool_call_id, String(event.args_preview || ""));
203
+ }
204
+ }
205
+
206
+ function takePlanInput(event) {
207
+ if (event.tool_name !== "update_plan" || !event.tool_call_id) {
208
+ return "";
209
+ }
210
+ const value = pendingPlanInputs.get(event.tool_call_id) || "";
211
+ pendingPlanInputs.delete(event.tool_call_id);
212
+ return value;
213
+ }
214
+
215
+ return {
216
+ handle,
217
+ closeAssistant: () => output.closeAssistant?.(),
218
+ resetTurnState,
219
+ };
220
+ }
221
+
222
+ function parsePlanInput(value) {
223
+ const args = parseObject(value);
224
+ return Array.isArray(args.plan) ? args.plan : null;
225
+ }
226
+
227
+ function parseToolData(value) {
228
+ const parsed = parseObject(value);
229
+ return parsed.data && typeof parsed.data === "object" ? parsed.data : {};
230
+ }
231
+
232
+ function parseObject(value) {
233
+ if (value && typeof value === "object") {
234
+ return value;
235
+ }
236
+ try {
237
+ const parsed = JSON.parse(String(value || ""));
238
+ return parsed && typeof parsed === "object" ? parsed : {};
239
+ } catch {
240
+ return {};
241
+ }
242
+ }