@rind-ai/cli 0.4.1 → 0.6.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.
- package/bin/rind.js +5 -5
- package/lib/assistant-renderer.js +179 -265
- package/lib/choice-menu-state.js +46 -46
- package/lib/cli-input-actions.js +548 -0
- package/lib/cli-output-controller.js +460 -0
- package/lib/cli-runtime-controller.js +350 -0
- package/lib/cli-state-store.js +32 -0
- package/lib/cli-state.js +41 -0
- package/lib/command-controller.js +159 -126
- package/lib/compact-context-state.js +22 -22
- package/lib/components/assistant-message.js +169 -0
- package/lib/components/composer-area.js +25 -0
- package/lib/components/dynamic-block.js +20 -0
- package/lib/components/monitor-stack.js +35 -0
- package/lib/components/text-block.js +47 -0
- package/lib/components/tool-block.js +122 -0
- package/lib/composer-terminal.js +224 -203
- package/lib/event-controller.js +243 -242
- package/lib/frontend-cli-implementation.js +656 -1111
- package/lib/input-controller.js +75 -94
- package/lib/input-errors.js +3 -3
- package/lib/interrupt-state.js +9 -9
- package/lib/line-editor.js +541 -541
- package/lib/local-slash-commands.js +217 -0
- package/lib/markdown-lines.js +103 -0
- package/lib/model-menu-state.js +50 -50
- package/lib/one-shot-progress.js +145 -0
- package/lib/one-shot.js +228 -0
- package/lib/question-menu-state.js +61 -0
- package/lib/rendering.js +1295 -1037
- package/lib/runtime-client.js +241 -193
- package/lib/runtime-env.js +21 -21
- package/lib/runtime-protocol.js +122 -15
- package/lib/slash-command-mode.js +16 -27
- package/lib/slash-menu-state.js +59 -59
- package/lib/{background-controller.js → task-monitor-controller.js} +411 -289
- package/lib/terminal-key.js +97 -97
- package/lib/text-width.js +335 -151
- package/lib/theme-menu-state.js +31 -0
- package/lib/theme.js +134 -0
- package/lib/tool-display.js +680 -0
- package/lib/tui/component.js +55 -0
- package/lib/tui/cursor.js +29 -0
- package/lib/tui/input-buffer.js +172 -0
- package/lib/tui/tui.js +591 -0
- package/lib/turn-controller.js +68 -78
- package/package.json +28 -28
- package/lib/assistant-stream-buffer.js +0 -25
- package/lib/terminal-ui.js +0 -581
package/lib/one-shot.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { createRuntimeClient } from "./runtime-client.js";
|
|
5
|
+
import { requireRuntimeInitialization, runtimeMethods } from "./runtime-protocol.js";
|
|
6
|
+
import { createOneShotProgress } from "./one-shot-progress.js";
|
|
7
|
+
|
|
8
|
+
export const oneShotHelp = [
|
|
9
|
+
"Usage: rind run --prompt <text> [--dir <absolute-path>] [--session <id>]",
|
|
10
|
+
"",
|
|
11
|
+
"Runs one prompt without the interactive TTY UI. The final assistant reply is written to stdout.",
|
|
12
|
+
"",
|
|
13
|
+
"Example:",
|
|
14
|
+
' rind run --prompt "Summarize the changes in src/" --dir "E:\\code\\my-project" --session 20260825_101530_ab12cd34',
|
|
15
|
+
].join("\n");
|
|
16
|
+
|
|
17
|
+
export const cliHelp = [
|
|
18
|
+
"Usage: rind [options]",
|
|
19
|
+
"",
|
|
20
|
+
"Start the interactive CLI.",
|
|
21
|
+
"",
|
|
22
|
+
oneShotHelp,
|
|
23
|
+
].join("\n");
|
|
24
|
+
|
|
25
|
+
export function parseOneShotArgs(args) {
|
|
26
|
+
if (args[0] !== "run") return null;
|
|
27
|
+
const result = { dir: null, session: null, prompt: null, debug: false, traceLlm: false };
|
|
28
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
29
|
+
const flag = args[index];
|
|
30
|
+
if (flag === "--debug") {
|
|
31
|
+
result.debug = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (flag === "--trace-llm") {
|
|
35
|
+
result.traceLlm = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!["--dir", "--session", "--prompt"].includes(flag)) {
|
|
39
|
+
throw new Error(`Unknown run option: ${flag}`);
|
|
40
|
+
}
|
|
41
|
+
const value = args[index + 1];
|
|
42
|
+
if (value === undefined || value.startsWith("--")) {
|
|
43
|
+
throw new Error(`${flag} requires a value.`);
|
|
44
|
+
}
|
|
45
|
+
index += 1;
|
|
46
|
+
const key = { "--dir": "dir", "--session": "session", "--prompt": "prompt" }[flag];
|
|
47
|
+
if (result[key] !== null) throw new Error(`${flag} may only be specified once.`);
|
|
48
|
+
result[key] = value;
|
|
49
|
+
}
|
|
50
|
+
if (!result.prompt?.trim()) throw new Error("--prompt requires a non-empty value.");
|
|
51
|
+
if (result.dir && !path.isAbsolute(result.dir)) {
|
|
52
|
+
throw new Error("--dir must be an absolute path.");
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function promptSlug(prompt) {
|
|
58
|
+
const slug = String(prompt || "")
|
|
59
|
+
.normalize("NFKC")
|
|
60
|
+
.replace(/[<>:"/\\|?*\x00-\x1f]/g, " ")
|
|
61
|
+
.replace(/\s+/g, " ")
|
|
62
|
+
.trim()
|
|
63
|
+
.slice(0, 40)
|
|
64
|
+
.trim();
|
|
65
|
+
return slug || "prompt";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function logFileName({ sessionId, prompt, now = new Date(), suffix = "" }) {
|
|
69
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "").replace("T", "_");
|
|
70
|
+
return `${stamp}_${sessionId}_${promptSlug(prompt)}${suffix}.md`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function writeRunLog({ workspace, sessionId, turnId, model, prompt, assistant, status, toolCount, error, startedAt = new Date(), finishedAt = new Date() }) {
|
|
74
|
+
const logsDir = path.join(workspace, "logs");
|
|
75
|
+
await mkdir(logsDir, { recursive: true });
|
|
76
|
+
let fileName = logFileName({ sessionId, prompt, now: finishedAt });
|
|
77
|
+
let target = path.join(logsDir, fileName);
|
|
78
|
+
let suffix = 0;
|
|
79
|
+
while (true) {
|
|
80
|
+
try {
|
|
81
|
+
const frontMatter = [
|
|
82
|
+
"---",
|
|
83
|
+
`session_id: ${yamlValue(sessionId)}`,
|
|
84
|
+
`turn_id: ${yamlValue(turnId)}`,
|
|
85
|
+
`workspace: ${yamlValue(workspace)}`,
|
|
86
|
+
`model: ${yamlValue(model)}`,
|
|
87
|
+
`status: ${yamlValue(status)}`,
|
|
88
|
+
`tool_count: ${Number(toolCount) || 0}`,
|
|
89
|
+
`started_at: ${yamlValue(startedAt.toISOString())}`,
|
|
90
|
+
`finished_at: ${yamlValue(finishedAt.toISOString())}`,
|
|
91
|
+
...(error ? [`error: ${yamlValue(error)}`] : []),
|
|
92
|
+
"---",
|
|
93
|
+
"",
|
|
94
|
+
"# Prompt",
|
|
95
|
+
"",
|
|
96
|
+
String(prompt || ""),
|
|
97
|
+
"",
|
|
98
|
+
"# Assistant",
|
|
99
|
+
"",
|
|
100
|
+
String(assistant || ""),
|
|
101
|
+
"",
|
|
102
|
+
].join("\n");
|
|
103
|
+
await writeFile(target, frontMatter, { encoding: "utf8", flag: "wx" });
|
|
104
|
+
return target;
|
|
105
|
+
} catch (errorValue) {
|
|
106
|
+
if (errorValue?.code !== "EEXIST" || suffix >= 9) throw errorValue;
|
|
107
|
+
suffix += 1;
|
|
108
|
+
fileName = logFileName({ sessionId, prompt, now: finishedAt, suffix: `_${suffix}` });
|
|
109
|
+
target = path.join(logsDir, fileName);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function runOneShot({ args, python, repoRoot, runtimePath, cwd = process.cwd(), stderr = console.error, stdout = console.log, clientFactory = createRuntimeClient }) {
|
|
115
|
+
const options = parseOneShotArgs(args);
|
|
116
|
+
if (!options) return false;
|
|
117
|
+
const runtimeArgs = ["--no-user-question"];
|
|
118
|
+
if (options.dir) runtimeArgs.push("--cwd", options.dir);
|
|
119
|
+
if (options.session) runtimeArgs.push("--session", options.session);
|
|
120
|
+
if (options.debug) runtimeArgs.push("--debug");
|
|
121
|
+
if (options.traceLlm) runtimeArgs.push("--trace-llm");
|
|
122
|
+
|
|
123
|
+
let assistant = "";
|
|
124
|
+
let completed = "";
|
|
125
|
+
let turnId = "";
|
|
126
|
+
let status = "failed";
|
|
127
|
+
const startedAt = new Date();
|
|
128
|
+
let sessionInfo = null;
|
|
129
|
+
const progress = createOneShotProgress({ stderr, stream: process.stderr });
|
|
130
|
+
let anonymousToolCounter = 0;
|
|
131
|
+
let client;
|
|
132
|
+
try {
|
|
133
|
+
progress.begin();
|
|
134
|
+
client = clientFactory({
|
|
135
|
+
python,
|
|
136
|
+
repoRoot,
|
|
137
|
+
runtimePath,
|
|
138
|
+
cwd: options.dir || cwd,
|
|
139
|
+
cliArgs: runtimeArgs,
|
|
140
|
+
onMessage: (message) => {
|
|
141
|
+
const event = message?.event;
|
|
142
|
+
const type = event?.type;
|
|
143
|
+
if (message?.turn_id) turnId = String(message.turn_id);
|
|
144
|
+
if (type === "assistant_delta") assistant += String(event.text || "");
|
|
145
|
+
if (type === "assistant_message_completed") completed = String(event.content || "");
|
|
146
|
+
if (type === "goal_continued") {
|
|
147
|
+
progress.note(`goal check · round ${Number(event.round) || 0}`);
|
|
148
|
+
}
|
|
149
|
+
const toolCallId = String(event?.tool_call_id || "");
|
|
150
|
+
const trackedId = toolCallId || (type === "tool_requested" ? `anon:${(anonymousToolCounter += 1)}` : "");
|
|
151
|
+
if (trackedId && (type === "tool_requested" || type === "tool_input_started") && !progress.hasTool(trackedId)) {
|
|
152
|
+
progress.toolStarted(trackedId, String(event.tool_name || "tool"));
|
|
153
|
+
}
|
|
154
|
+
if (trackedId && type === "tool_result") {
|
|
155
|
+
const failed = Boolean(event.error_type) || ["error", "failed"].includes(String(event.status || ""));
|
|
156
|
+
progress.toolFinished(trackedId, { ok: !failed, durationMs: Number(event.duration_ms) || 0 });
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
onStderr: (text) => {
|
|
160
|
+
if (options.debug) stderr(`${String(text).trimEnd()}\n`);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
client.start();
|
|
164
|
+
sessionInfo = requireRuntimeInitialization(await client.request(runtimeMethods.initialize));
|
|
165
|
+
const sessionId = String(sessionInfo.session_id || options.session || "").trim();
|
|
166
|
+
if (!sessionId) throw new Error("Runtime initialization did not return a session_id.");
|
|
167
|
+
progress.session({
|
|
168
|
+
sessionId,
|
|
169
|
+
model: String(sessionInfo.model || ""),
|
|
170
|
+
baseUrl: String(sessionInfo.base_url || ""),
|
|
171
|
+
});
|
|
172
|
+
const result = await client.request(runtimeMethods.sessionPrompt, {
|
|
173
|
+
session_id: sessionId,
|
|
174
|
+
input: options.prompt,
|
|
175
|
+
});
|
|
176
|
+
status = "completed";
|
|
177
|
+
const responseTurnId = String(result?.turn_id || turnId || "");
|
|
178
|
+
turnId = responseTurnId;
|
|
179
|
+
const finalText = completed || assistant;
|
|
180
|
+
const finishedAt = new Date();
|
|
181
|
+
const logPath = await writeRunLog({
|
|
182
|
+
workspace: cwd,
|
|
183
|
+
sessionId,
|
|
184
|
+
turnId,
|
|
185
|
+
model: sessionInfo.model || "",
|
|
186
|
+
prompt: options.prompt,
|
|
187
|
+
assistant: finalText,
|
|
188
|
+
status,
|
|
189
|
+
toolCount: progress.toolCount,
|
|
190
|
+
startedAt,
|
|
191
|
+
finishedAt,
|
|
192
|
+
});
|
|
193
|
+
progress.done(finishedAt - startedAt);
|
|
194
|
+
progress.note(`log ${logPath}`);
|
|
195
|
+
stdout(finalText);
|
|
196
|
+
return true;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
199
|
+
progress.fail(`run failed: ${message}`);
|
|
200
|
+
if (sessionInfo) {
|
|
201
|
+
try {
|
|
202
|
+
await writeRunLog({
|
|
203
|
+
workspace: cwd,
|
|
204
|
+
sessionId: String(sessionInfo.session_id || options.session || "unknown"),
|
|
205
|
+
turnId,
|
|
206
|
+
model: sessionInfo.model || "",
|
|
207
|
+
prompt: options.prompt,
|
|
208
|
+
assistant: completed || assistant,
|
|
209
|
+
status: "failed",
|
|
210
|
+
toolCount: progress.toolCount,
|
|
211
|
+
error: message,
|
|
212
|
+
startedAt,
|
|
213
|
+
finishedAt: new Date(),
|
|
214
|
+
});
|
|
215
|
+
} catch (logError) {
|
|
216
|
+
stderr(`Log failed: ${logError instanceof Error ? logError.message : String(logError)}\n`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
return true;
|
|
221
|
+
} finally {
|
|
222
|
+
if (client) await client.shutdown().catch(() => client.forceShutdown());
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function yamlValue(value) {
|
|
227
|
+
return JSON.stringify(String(value || ""));
|
|
228
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const CUSTOM_ANSWER_LABEL = "Type your own answer";
|
|
2
|
+
|
|
3
|
+
export function createQuestionMenuState(options) {
|
|
4
|
+
const items = normalizeOptions(options);
|
|
5
|
+
const customIndex = items.length;
|
|
6
|
+
let selected = 0;
|
|
7
|
+
let editing = false;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
options() {
|
|
11
|
+
return items;
|
|
12
|
+
},
|
|
13
|
+
selectedIndex() {
|
|
14
|
+
return selected;
|
|
15
|
+
},
|
|
16
|
+
selectedOption() {
|
|
17
|
+
return items[selected] || null;
|
|
18
|
+
},
|
|
19
|
+
isEditing() {
|
|
20
|
+
return editing;
|
|
21
|
+
},
|
|
22
|
+
enterEditing() {
|
|
23
|
+
if (selected !== customIndex) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
editing = true;
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
handleNavigation(key = {}) {
|
|
30
|
+
const vimNavigation = !editing && (key.text === "j" || key.text === "k");
|
|
31
|
+
if (key.name !== "up" && key.name !== "down" && !vimNavigation) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const total = items.length + 1;
|
|
35
|
+
if (key.name === "up" || key.text === "k") {
|
|
36
|
+
selected = selected <= 0 ? total - 1 : selected - 1;
|
|
37
|
+
} else {
|
|
38
|
+
selected = selected >= total - 1 ? 0 : selected + 1;
|
|
39
|
+
}
|
|
40
|
+
editing = false;
|
|
41
|
+
return true;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeOptions(options) {
|
|
47
|
+
const labels = new Set();
|
|
48
|
+
const items = [];
|
|
49
|
+
for (const option of Array.isArray(options) ? options : []) {
|
|
50
|
+
const label = String(option?.label || "").trim();
|
|
51
|
+
if (!label || labels.has(label)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
labels.add(label);
|
|
55
|
+
items.push({
|
|
56
|
+
label,
|
|
57
|
+
description: String(option?.description || "").trim(),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return items;
|
|
61
|
+
}
|