@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,201 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+
4
+ import { buildRuntimeEnv } from "./runtime-env.js";
5
+ import { createRuntimeRequest, runtimeRequestId } from "./runtime-protocol.js";
6
+
7
+ export function resolveRuntimeLaunch({ python, repoRoot, runtimePath = "", cliArgs = [] }) {
8
+ const executable = runtimePath
9
+ ? { command: runtimePath, args: [] }
10
+ : { command: python, args: [path.join(repoRoot, "main.py")] };
11
+ const traceFlag = isTraceLlmEnvSet() ? ["--trace-llm"] : [];
12
+ return {
13
+ command: executable.command,
14
+ args: [...executable.args, "app-server", "--stdio", ...traceFlag, ...cliArgs],
15
+ };
16
+ }
17
+
18
+ const TRACE_TRUTHY = new Set(["1", "true", "yes", "on"]);
19
+ export function isTraceLlmEnvSet() {
20
+ return TRACE_TRUTHY.has(String(process.env.RIND_TRACE_LLM || "").trim().toLowerCase());
21
+ }
22
+
23
+ export function runHelpVersion({ python, repoRoot, runtimePath = "", cliArgs, cwd = process.cwd() }) {
24
+ const executable = runtimePath
25
+ ? { command: runtimePath, args: [] }
26
+ : { command: python, args: [path.join(repoRoot, "main.py")] };
27
+ const result = spawnSync(executable.command, [...executable.args, ...cliArgs], {
28
+ cwd,
29
+ env: buildRuntimeEnv(repoRoot, process.env, { sourceRuntime: !runtimePath }),
30
+ stdio: "inherit",
31
+ });
32
+ return result.status ?? 1;
33
+ }
34
+
35
+ export function createRuntimeClient({
36
+ python,
37
+ repoRoot,
38
+ cliArgs = [],
39
+ cwd = process.cwd(),
40
+ rindHome = process.env.RIND_HOME,
41
+ runtimePath = process.env.RIND_RUNTIME_PATH || "",
42
+ onEvent = () => {},
43
+ onMessage = null,
44
+ onStderr = () => {},
45
+ onExit = () => {},
46
+ }) {
47
+ const handleEvent = onMessage || onEvent;
48
+ const launch = resolveRuntimeLaunch({ python, repoRoot, runtimePath, cliArgs });
49
+
50
+ let nextId = 1;
51
+ let stdoutBuffer = "";
52
+ let closing = false;
53
+ let killTimer = null;
54
+ let exitHandled = false;
55
+ const pending = new Map();
56
+ const child = spawn(launch.command, launch.args, {
57
+ cwd,
58
+ env: buildRuntimeEnv(repoRoot, process.env, {
59
+ sourceRuntime: !runtimePath,
60
+ rindHome,
61
+ }),
62
+ stdio: ["pipe", "pipe", "pipe"],
63
+ });
64
+
65
+ child.stdout.setEncoding("utf8");
66
+ child.stdout.on("data", (chunk) => {
67
+ stdoutBuffer += chunk;
68
+ const lines = stdoutBuffer.split(/\r?\n/);
69
+ stdoutBuffer = lines.pop() || "";
70
+ for (const line of lines) {
71
+ if (line) {
72
+ receive(line);
73
+ }
74
+ }
75
+ });
76
+ child.stderr.on("data", (chunk) => onStderr(chunk));
77
+ child.once("error", (error) => {
78
+ handleExit(null, null, error);
79
+ });
80
+ child.once("exit", (code, signal) => {
81
+ handleExit(code, signal);
82
+ });
83
+
84
+ function handleExit(code, signal, cause = null) {
85
+ if (exitHandled) {
86
+ return;
87
+ }
88
+ exitHandled = true;
89
+ clearKillTimer();
90
+ const error = cause || new Error(`Runtime exited with ${signal || code}`);
91
+ for (const { reject } of pending.values()) {
92
+ reject(error);
93
+ }
94
+ pending.clear();
95
+ onExit(code, signal, { closing, error });
96
+ }
97
+
98
+ function request(method, params = {}) {
99
+ const id = nextId++;
100
+ return new Promise((resolve, reject) => {
101
+ if (!child.stdin.writable || child.destroyed) {
102
+ reject(new Error("Runtime stdin is closed. Restart Rind and try again."));
103
+ return;
104
+ }
105
+ pending.set(id, { resolve, reject });
106
+ child.stdin.write(JSON.stringify(createRuntimeRequest(id, method, params)) + "\n", (error) => {
107
+ if (!error) {
108
+ return;
109
+ }
110
+ pending.delete(id);
111
+ reject(error);
112
+ });
113
+ });
114
+ }
115
+
116
+ function receive(line) {
117
+ let message;
118
+ try {
119
+ message = JSON.parse(line);
120
+ } catch {
121
+ return;
122
+ }
123
+ if (message.kind === "response") {
124
+ finishRequest(message);
125
+ return;
126
+ }
127
+ if (message.kind === "event") {
128
+ handleEvent(message);
129
+ }
130
+ }
131
+
132
+ function finishRequest(message) {
133
+ const id = runtimeRequestId(message);
134
+ const callbacks = pending.get(id);
135
+ if (!callbacks) {
136
+ return;
137
+ }
138
+ pending.delete(id);
139
+ if (message.error) {
140
+ callbacks.reject(new Error(message.error.message || "Runtime request failed"));
141
+ } else {
142
+ callbacks.resolve(message.result);
143
+ }
144
+ }
145
+
146
+ function shutdown() {
147
+ if (closing) {
148
+ return Promise.resolve();
149
+ }
150
+ closing = true;
151
+ scheduleKill();
152
+ return request("shutdown").catch(() => {
153
+ forceShutdown();
154
+ }).finally(() => {
155
+ if (child.stdin.writable) {
156
+ child.stdin.end();
157
+ }
158
+ });
159
+ }
160
+
161
+ function forceShutdown() {
162
+ closing = true;
163
+ clearKillTimer();
164
+ if (!child.killed && child.exitCode === null) {
165
+ try {
166
+ child.kill("SIGKILL");
167
+ } catch {
168
+ // Ignore kill races during shutdown.
169
+ }
170
+ }
171
+ }
172
+
173
+ function closeInput() {
174
+ if (child.stdin.writable) {
175
+ child.stdin.end();
176
+ }
177
+ }
178
+
179
+ function scheduleKill() {
180
+ clearKillTimer();
181
+ killTimer = setTimeout(forceShutdown, 1500);
182
+ killTimer.unref?.();
183
+ }
184
+
185
+ function clearKillTimer() {
186
+ if (!killTimer) {
187
+ return;
188
+ }
189
+ clearTimeout(killTimer);
190
+ killTimer = null;
191
+ }
192
+
193
+ return {
194
+ child,
195
+ request,
196
+ shutdown,
197
+ forceShutdown,
198
+ closeInput,
199
+ isClosing: () => closing,
200
+ };
201
+ }
@@ -0,0 +1,21 @@
1
+ import path from "node:path";
2
+
3
+ export function buildRuntimeEnv(repoRoot, baseEnv = process.env, { sourceRuntime = true, rindHome } = {}) {
4
+ const env = { ...baseEnv };
5
+ if (rindHome) {
6
+ env.RIND_HOME = rindHome;
7
+ }
8
+ if (!sourceRuntime) {
9
+ return env;
10
+ }
11
+ return {
12
+ ...env,
13
+ PYTHONIOENCODING: "utf-8",
14
+ PYTHONPATH: prependPath(repoRoot, baseEnv.PYTHONPATH),
15
+ PYTHONUTF8: "1",
16
+ };
17
+ }
18
+
19
+ function prependPath(entry, value) {
20
+ return value ? `${entry}${path.delimiter}${value}` : entry;
21
+ }
@@ -0,0 +1,15 @@
1
+ export function createRuntimeRequest(requestId, method, params = {}) {
2
+ return { request_id: requestId, method, params };
3
+ }
4
+
5
+ export function runtimeRequestId(message) {
6
+ return message?.request_id;
7
+ }
8
+
9
+ export function runtimeEventType(message) {
10
+ return message?.event_type || message?.event?.type || "";
11
+ }
12
+
13
+ export function turnInputMethod(activeTurn) {
14
+ return activeTurn ? "turn.follow_up" : "turn.start";
15
+ }
@@ -0,0 +1,27 @@
1
+ export function isReadonlySlashCommand(value) {
2
+ const text = String(value || "").trim().toLowerCase();
3
+ return text === "/status" || text.startsWith("/status ") || text === "/doctor" || text.startsWith("/doctor ");
4
+ }
5
+
6
+ export function steeringCommandText(value) {
7
+ const text = String(value || "").trim();
8
+ const match = text.match(/^\/steer(?:\s+([\s\S]*))?$/i);
9
+ return match ? String(match[1] || "").trim() : null;
10
+ }
11
+
12
+ export function parseGoalCommand(value) {
13
+ const text = String(value || "").trim();
14
+ const match = text.match(/^\/goal(?:\s+([\s\S]*))?$/i);
15
+ if (!match) {
16
+ return null;
17
+ }
18
+ const argument = String(match[1] || "").trim();
19
+ const action = argument.toLowerCase();
20
+ if (!argument) {
21
+ return { action: "get" };
22
+ }
23
+ if (["pause", "resume", "clear"].includes(action)) {
24
+ return { action };
25
+ }
26
+ return { action: "set", objective: argument };
27
+ }
@@ -0,0 +1,59 @@
1
+ export function createSlashMenuState(commands) {
2
+ let text = "";
3
+ let selected = 0;
4
+ let dismissed = false;
5
+ return {
6
+ input() {
7
+ return text;
8
+ },
9
+ matches() {
10
+ return dismissed ? [] : matchingCommands(commands, text);
11
+ },
12
+ selectedIndex() {
13
+ return selected;
14
+ },
15
+ selectedCommand() {
16
+ return this.matches()[selected] || null;
17
+ },
18
+ handleKey(chunk, key = {}) {
19
+ if (key.name === "escape") {
20
+ dismissed = true;
21
+ selected = 0;
22
+ return true;
23
+ }
24
+ const matches = this.matches();
25
+ if (matches.length && key.name === "up") {
26
+ selected = selected <= 0 ? matches.length - 1 : selected - 1;
27
+ return true;
28
+ }
29
+ if (matches.length && key.name === "down") {
30
+ selected = selected >= matches.length - 1 ? 0 : selected + 1;
31
+ return true;
32
+ }
33
+ if (chunk && !key.ctrl && !key.meta && String(chunk) >= " ") {
34
+ text += String(chunk);
35
+ selected = 0;
36
+ dismissed = false;
37
+ return true;
38
+ }
39
+ return false;
40
+ },
41
+ setInput(value) {
42
+ const nextText = String(value || "");
43
+ if (nextText !== text) {
44
+ selected = 0;
45
+ dismissed = false;
46
+ }
47
+ text = nextText;
48
+ },
49
+ };
50
+ }
51
+
52
+ function matchingCommands(commands, line) {
53
+ const text = String(line || "");
54
+ if (!text.startsWith("/") || /\s/.test(text)) {
55
+ return [];
56
+ }
57
+ const token = text.slice(1).toLowerCase();
58
+ return commands.filter((command) => command.name.startsWith(token));
59
+ }
@@ -0,0 +1,97 @@
1
+ const ARROW_KEYS = {
2
+ A: "up",
3
+ B: "down",
4
+ C: "right",
5
+ D: "left",
6
+ H: "home",
7
+ F: "end",
8
+ };
9
+
10
+ export function parseTerminalKey(raw = "") {
11
+ const value = String(raw || "");
12
+ if (!value) {
13
+ return null;
14
+ }
15
+ if (value === "\r" || value === "\x1bOM") {
16
+ return key("enter");
17
+ }
18
+ if (value === "\n") {
19
+ return key("j", 5);
20
+ }
21
+ if (value === "\t") {
22
+ return key("tab");
23
+ }
24
+ if (value === "\b") {
25
+ return key("backspace", process.platform === "win32" && process.env.WT_SESSION ? 5 : 1);
26
+ }
27
+ if (value === "\x7f") {
28
+ return key("backspace");
29
+ }
30
+ if (value === "\x1f") {
31
+ return key("-", 5);
32
+ }
33
+ if (value === "\x1b") {
34
+ return key("escape");
35
+ }
36
+ if (value.length === 1) {
37
+ const code = value.charCodeAt(0);
38
+ if (code >= 1 && code <= 26) {
39
+ return key(String.fromCharCode(96 + code), 5);
40
+ }
41
+ if (code < 32) {
42
+ return null;
43
+ }
44
+ return { kind: "text", name: "", text: value };
45
+ }
46
+
47
+ const modifiedArrow = value.match(/^\x1b\[1;([2-8])([ABCDHF])$/);
48
+ if (modifiedArrow) {
49
+ return key(ARROW_KEYS[modifiedArrow[2]], Number(modifiedArrow[1]));
50
+ }
51
+ const tilde = value.match(/^\x1b\[([0-9]+)(?:;([2-8]))?~$/);
52
+ if (tilde) {
53
+ return Number(tilde[1]) === 3 ? key("delete", Number(tilde[2] || 1)) : null;
54
+ }
55
+ const csiKey = value.match(/^\x1b\[([ABCDHFZ])$/);
56
+ if (csiKey) {
57
+ return csiKey[1] === "Z" ? key("tab", 2) : key(ARROW_KEYS[csiKey[1]]);
58
+ }
59
+ const ss3Key = value.match(/^\x1bO([ABCDHF])$/);
60
+ if (ss3Key) {
61
+ return key(ARROW_KEYS[ss3Key[1]]);
62
+ }
63
+ const csiEnter = value.match(/^\x1b\[13;([2-8])u$/);
64
+ if (csiEnter) {
65
+ return key("enter", Number(csiEnter[1]));
66
+ }
67
+ const csiMinus = value.match(/^\x1b\[45;([2-8])u$/);
68
+ if (csiMinus) {
69
+ return key("-", Number(csiMinus[1]));
70
+ }
71
+ const modifiedEnter = value.match(/^\x1b\[27;([2-8]);13~$/);
72
+ if (modifiedEnter) {
73
+ return key("enter", Number(modifiedEnter[1]));
74
+ }
75
+ if (value === "\x1b\r") {
76
+ return key("enter", 2);
77
+ }
78
+ if (value.startsWith("\x1b") && value.length === 2) {
79
+ return key(value[1] === "\x7f" || value[1] === "\b" ? "backspace" : value[1], 3);
80
+ }
81
+ if (value.startsWith("\x1b")) {
82
+ return null;
83
+ }
84
+ return { kind: "text", name: "", text: value };
85
+ }
86
+
87
+ function key(name, modifier = 1) {
88
+ const bits = modifier - 1;
89
+ return {
90
+ kind: "key",
91
+ name,
92
+ shift: Boolean(bits & 1),
93
+ alt: Boolean(bits & 2),
94
+ ctrl: Boolean(bits & 4),
95
+ text: "",
96
+ };
97
+ }