@robota-sdk/agent-cli 3.0.0-beta.3 → 3.0.0-beta.31
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/README.md +12 -3
- package/dist/node/bin.cjs +1725 -625
- package/dist/node/bin.js +11 -1
- package/dist/node/chunk-2CGAQADC.js +2292 -0
- package/dist/node/index.cjs +1723 -633
- package/dist/node/index.js +1 -1
- package/package.json +4 -3
- package/dist/node/chunk-MO7RIZFR.js +0 -1198
|
@@ -0,0 +1,2292 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3
|
+
import { join as join5, dirname as dirname3 } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import {
|
|
6
|
+
loadConfig,
|
|
7
|
+
loadContext,
|
|
8
|
+
detectProject,
|
|
9
|
+
createSession as createSession2,
|
|
10
|
+
SessionStore,
|
|
11
|
+
FileSessionLogger as FileSessionLogger2,
|
|
12
|
+
projectPaths as projectPaths2
|
|
13
|
+
} from "@robota-sdk/agent-sdk";
|
|
14
|
+
import { promptForApproval } from "@robota-sdk/agent-sdk";
|
|
15
|
+
|
|
16
|
+
// src/utils/cli-args.ts
|
|
17
|
+
import { parseArgs } from "util";
|
|
18
|
+
var VALID_MODES = ["plan", "default", "acceptEdits", "bypassPermissions"];
|
|
19
|
+
function parsePermissionMode(raw) {
|
|
20
|
+
if (raw === void 0) return void 0;
|
|
21
|
+
if (!VALID_MODES.includes(raw)) {
|
|
22
|
+
process.stderr.write(`Invalid --permission-mode "${raw}". Valid: ${VALID_MODES.join(" | ")}
|
|
23
|
+
`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
return raw;
|
|
27
|
+
}
|
|
28
|
+
function parseMaxTurns(raw) {
|
|
29
|
+
if (raw === void 0) return void 0;
|
|
30
|
+
const n = parseInt(raw, 10);
|
|
31
|
+
if (isNaN(n) || n <= 0) {
|
|
32
|
+
process.stderr.write(`Invalid --max-turns "${raw}". Must be a positive integer.
|
|
33
|
+
`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
38
|
+
function parseCliArgs() {
|
|
39
|
+
const { values, positionals } = parseArgs({
|
|
40
|
+
allowPositionals: true,
|
|
41
|
+
options: {
|
|
42
|
+
p: { type: "boolean", short: "p", default: false },
|
|
43
|
+
c: { type: "boolean", short: "c", default: false },
|
|
44
|
+
r: { type: "string", short: "r" },
|
|
45
|
+
model: { type: "string" },
|
|
46
|
+
language: { type: "string" },
|
|
47
|
+
"permission-mode": { type: "string" },
|
|
48
|
+
"max-turns": { type: "string" },
|
|
49
|
+
version: { type: "boolean", default: false },
|
|
50
|
+
reset: { type: "boolean", default: false }
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
positional: positionals,
|
|
55
|
+
printMode: values["p"] ?? false,
|
|
56
|
+
continueMode: values["c"] ?? false,
|
|
57
|
+
resumeId: values["r"],
|
|
58
|
+
model: values["model"],
|
|
59
|
+
language: values["language"],
|
|
60
|
+
permissionMode: parsePermissionMode(values["permission-mode"]),
|
|
61
|
+
maxTurns: parseMaxTurns(values["max-turns"]),
|
|
62
|
+
version: values["version"] ?? false,
|
|
63
|
+
reset: values["reset"] ?? false
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/utils/settings-io.ts
|
|
68
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
69
|
+
import { join, dirname } from "path";
|
|
70
|
+
function getUserSettingsPath() {
|
|
71
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
|
|
72
|
+
return join(home, ".robota", "settings.json");
|
|
73
|
+
}
|
|
74
|
+
function readSettings(path) {
|
|
75
|
+
if (!existsSync(path)) return {};
|
|
76
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
77
|
+
}
|
|
78
|
+
function writeSettings(path, settings) {
|
|
79
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
80
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
81
|
+
}
|
|
82
|
+
function updateModelInSettings(settingsPath, modelId) {
|
|
83
|
+
const settings = readSettings(settingsPath);
|
|
84
|
+
const provider = settings.provider ?? {};
|
|
85
|
+
provider.model = modelId;
|
|
86
|
+
settings.provider = provider;
|
|
87
|
+
writeSettings(settingsPath, settings);
|
|
88
|
+
}
|
|
89
|
+
function deleteSettings(path) {
|
|
90
|
+
if (existsSync(path)) {
|
|
91
|
+
unlinkSync(path);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/print-terminal.ts
|
|
98
|
+
import * as readline from "readline";
|
|
99
|
+
var PrintTerminal = class {
|
|
100
|
+
write(text) {
|
|
101
|
+
process.stdout.write(text);
|
|
102
|
+
}
|
|
103
|
+
writeLine(text) {
|
|
104
|
+
process.stdout.write(text + "\n");
|
|
105
|
+
}
|
|
106
|
+
writeMarkdown(md) {
|
|
107
|
+
process.stdout.write(md);
|
|
108
|
+
}
|
|
109
|
+
writeError(text) {
|
|
110
|
+
process.stderr.write(text + "\n");
|
|
111
|
+
}
|
|
112
|
+
prompt(question) {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const rl = readline.createInterface({
|
|
115
|
+
input: process.stdin,
|
|
116
|
+
output: process.stdout,
|
|
117
|
+
terminal: false,
|
|
118
|
+
historySize: 0
|
|
119
|
+
});
|
|
120
|
+
rl.question(question, (answer) => {
|
|
121
|
+
rl.close();
|
|
122
|
+
resolve(answer);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async select(options, initialIndex = 0) {
|
|
127
|
+
for (let i = 0; i < options.length; i++) {
|
|
128
|
+
const marker = i === initialIndex ? ">" : " ";
|
|
129
|
+
process.stdout.write(` ${marker} ${i + 1}) ${options[i]}
|
|
130
|
+
`);
|
|
131
|
+
}
|
|
132
|
+
const answer = await this.prompt(
|
|
133
|
+
` Choose [1-${options.length}] (default: ${options[initialIndex]}): `
|
|
134
|
+
);
|
|
135
|
+
const trimmed = answer.trim().toLowerCase();
|
|
136
|
+
if (trimmed === "") return initialIndex;
|
|
137
|
+
const num = parseInt(trimmed, 10);
|
|
138
|
+
if (!isNaN(num) && num >= 1 && num <= options.length) return num - 1;
|
|
139
|
+
return initialIndex;
|
|
140
|
+
}
|
|
141
|
+
spinner(_message) {
|
|
142
|
+
return { stop() {
|
|
143
|
+
}, update() {
|
|
144
|
+
} };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/ui/render.tsx
|
|
149
|
+
import { render } from "ink";
|
|
150
|
+
|
|
151
|
+
// src/ui/App.tsx
|
|
152
|
+
import { useState as useState7, useRef as useRef5 } from "react";
|
|
153
|
+
import { Box as Box9, Text as Text11, useApp, useInput as useInput5 } from "ink";
|
|
154
|
+
import { getModelName } from "@robota-sdk/agent-core";
|
|
155
|
+
|
|
156
|
+
// src/ui/hooks/useSession.ts
|
|
157
|
+
import { useState, useCallback, useRef } from "react";
|
|
158
|
+
import { createSession, FileSessionLogger, projectPaths } from "@robota-sdk/agent-sdk";
|
|
159
|
+
|
|
160
|
+
// src/utils/edit-diff.ts
|
|
161
|
+
function generateDiffLines(oldStr, newStr) {
|
|
162
|
+
if (oldStr === newStr) return [];
|
|
163
|
+
const lines = [];
|
|
164
|
+
for (const line of oldStr.split("\n")) {
|
|
165
|
+
lines.push({ type: "remove", text: line });
|
|
166
|
+
}
|
|
167
|
+
for (const line of newStr.split("\n")) {
|
|
168
|
+
lines.push({ type: "add", text: line });
|
|
169
|
+
}
|
|
170
|
+
return lines;
|
|
171
|
+
}
|
|
172
|
+
function extractEditDiff(toolName, toolArgs) {
|
|
173
|
+
if (toolName !== "Edit" || !toolArgs) return null;
|
|
174
|
+
const filePath = toolArgs.file_path ?? toolArgs.filePath;
|
|
175
|
+
const oldStr = toolArgs.old_string ?? toolArgs.oldString;
|
|
176
|
+
const newStr = toolArgs.new_string ?? toolArgs.newString;
|
|
177
|
+
if (typeof filePath !== "string") return null;
|
|
178
|
+
if (typeof oldStr !== "string" || typeof newStr !== "string") return null;
|
|
179
|
+
const lines = generateDiffLines(oldStr, newStr);
|
|
180
|
+
if (lines.length === 0) return null;
|
|
181
|
+
return { file: filePath, lines };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/ui/hooks/useSession.ts
|
|
185
|
+
var TOOL_ARG_DISPLAY_MAX = 80;
|
|
186
|
+
var TAIL_KEEP = 30;
|
|
187
|
+
var MAX_COMPLETED_TOOLS = 50;
|
|
188
|
+
var NOOP_TERMINAL = {
|
|
189
|
+
write: () => {
|
|
190
|
+
},
|
|
191
|
+
writeLine: () => {
|
|
192
|
+
},
|
|
193
|
+
writeMarkdown: () => {
|
|
194
|
+
},
|
|
195
|
+
writeError: () => {
|
|
196
|
+
},
|
|
197
|
+
prompt: () => Promise.resolve(""),
|
|
198
|
+
select: () => Promise.resolve(0),
|
|
199
|
+
spinner: () => ({ stop: () => {
|
|
200
|
+
}, update: () => {
|
|
201
|
+
} })
|
|
202
|
+
};
|
|
203
|
+
function useSession(props) {
|
|
204
|
+
const [permissionRequest, setPermissionRequest] = useState(null);
|
|
205
|
+
const [streamingText, setStreamingText] = useState("");
|
|
206
|
+
const [activeTools, setActiveTools] = useState([]);
|
|
207
|
+
const permissionQueueRef = useRef([]);
|
|
208
|
+
const processingRef = useRef(false);
|
|
209
|
+
const processNextPermission = useCallback(() => {
|
|
210
|
+
if (processingRef.current) return;
|
|
211
|
+
const next = permissionQueueRef.current[0];
|
|
212
|
+
if (!next) {
|
|
213
|
+
setPermissionRequest(null);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
processingRef.current = true;
|
|
217
|
+
setPermissionRequest({
|
|
218
|
+
toolName: next.toolName,
|
|
219
|
+
toolArgs: next.toolArgs,
|
|
220
|
+
resolve: (result) => {
|
|
221
|
+
permissionQueueRef.current.shift();
|
|
222
|
+
processingRef.current = false;
|
|
223
|
+
setPermissionRequest(null);
|
|
224
|
+
next.resolve(result);
|
|
225
|
+
setTimeout(() => processNextPermission(), 0);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}, []);
|
|
229
|
+
const sessionRef = useRef(null);
|
|
230
|
+
if (sessionRef.current === null) {
|
|
231
|
+
const permissionHandler = (toolName, toolArgs) => {
|
|
232
|
+
return new Promise((resolve) => {
|
|
233
|
+
permissionQueueRef.current.push({ toolName, toolArgs, resolve });
|
|
234
|
+
processNextPermission();
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
const onTextDelta = (delta) => {
|
|
238
|
+
setStreamingText((prev) => prev + delta);
|
|
239
|
+
};
|
|
240
|
+
const onToolExecution = (event) => {
|
|
241
|
+
if (event.type === "start") {
|
|
242
|
+
let firstArg = "";
|
|
243
|
+
if (event.toolArgs) {
|
|
244
|
+
const firstVal = Object.values(event.toolArgs)[0];
|
|
245
|
+
const raw = typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal ?? "");
|
|
246
|
+
firstArg = raw.length > TOOL_ARG_DISPLAY_MAX ? raw.slice(0, TOOL_ARG_DISPLAY_MAX - TAIL_KEEP - 3) + "..." + raw.slice(-TAIL_KEEP) : raw;
|
|
247
|
+
}
|
|
248
|
+
setActiveTools((prev) => [
|
|
249
|
+
...prev,
|
|
250
|
+
{ toolName: event.toolName, firstArg, isRunning: true, _toolArgs: event.toolArgs }
|
|
251
|
+
]);
|
|
252
|
+
} else {
|
|
253
|
+
const toolResult = event.denied ? "denied" : event.success === false ? "error" : "success";
|
|
254
|
+
setActiveTools((prev) => {
|
|
255
|
+
const updated = prev.map((t) => {
|
|
256
|
+
if (!(t.toolName === event.toolName && t.isRunning)) return t;
|
|
257
|
+
const editDiff = extractEditDiff(
|
|
258
|
+
event.toolName,
|
|
259
|
+
t._toolArgs
|
|
260
|
+
);
|
|
261
|
+
const finished = {
|
|
262
|
+
...t,
|
|
263
|
+
isRunning: false,
|
|
264
|
+
result: toolResult
|
|
265
|
+
};
|
|
266
|
+
if (editDiff) {
|
|
267
|
+
finished.diffLines = editDiff.lines;
|
|
268
|
+
finished.diffFile = editDiff.file;
|
|
269
|
+
}
|
|
270
|
+
delete finished._toolArgs;
|
|
271
|
+
return finished;
|
|
272
|
+
});
|
|
273
|
+
const completed = updated.filter((t) => !t.isRunning);
|
|
274
|
+
if (completed.length > MAX_COMPLETED_TOOLS) {
|
|
275
|
+
const excess = completed.length - MAX_COMPLETED_TOOLS;
|
|
276
|
+
let removed = 0;
|
|
277
|
+
return updated.filter((t) => {
|
|
278
|
+
if (!t.isRunning && removed < excess) {
|
|
279
|
+
removed++;
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return updated;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const paths = projectPaths(props.cwd ?? process.cwd());
|
|
290
|
+
sessionRef.current = createSession({
|
|
291
|
+
config: props.config,
|
|
292
|
+
context: props.context,
|
|
293
|
+
terminal: NOOP_TERMINAL,
|
|
294
|
+
sessionLogger: new FileSessionLogger(paths.logs),
|
|
295
|
+
projectInfo: props.projectInfo,
|
|
296
|
+
sessionStore: props.sessionStore,
|
|
297
|
+
permissionMode: props.permissionMode,
|
|
298
|
+
maxTurns: props.maxTurns,
|
|
299
|
+
permissionHandler,
|
|
300
|
+
onTextDelta,
|
|
301
|
+
onToolExecution
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const clearStreamingText = useCallback(() => {
|
|
305
|
+
setStreamingText("");
|
|
306
|
+
setActiveTools([]);
|
|
307
|
+
}, []);
|
|
308
|
+
return {
|
|
309
|
+
session: sessionRef.current,
|
|
310
|
+
permissionRequest,
|
|
311
|
+
streamingText,
|
|
312
|
+
clearStreamingText,
|
|
313
|
+
activeTools
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/ui/hooks/useMessages.ts
|
|
318
|
+
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
319
|
+
var MAX_RENDERED_MESSAGES = 100;
|
|
320
|
+
var msgIdCounter = 0;
|
|
321
|
+
function nextId() {
|
|
322
|
+
msgIdCounter += 1;
|
|
323
|
+
return `msg_${msgIdCounter}`;
|
|
324
|
+
}
|
|
325
|
+
function useMessages() {
|
|
326
|
+
const [messages, setMessages] = useState2([]);
|
|
327
|
+
const addMessage = useCallback2((msg) => {
|
|
328
|
+
setMessages((prev) => {
|
|
329
|
+
const updated = [...prev, { ...msg, id: nextId(), timestamp: /* @__PURE__ */ new Date() }];
|
|
330
|
+
if (updated.length > MAX_RENDERED_MESSAGES) {
|
|
331
|
+
return updated.slice(-MAX_RENDERED_MESSAGES);
|
|
332
|
+
}
|
|
333
|
+
return updated;
|
|
334
|
+
});
|
|
335
|
+
}, []);
|
|
336
|
+
return { messages, setMessages, addMessage };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/ui/hooks/useSlashCommands.ts
|
|
340
|
+
import { useCallback as useCallback3 } from "react";
|
|
341
|
+
|
|
342
|
+
// src/commands/slash-executor.ts
|
|
343
|
+
var VALID_MODES2 = ["plan", "default", "acceptEdits", "bypassPermissions"];
|
|
344
|
+
var HELP_TEXT = [
|
|
345
|
+
"Available commands:",
|
|
346
|
+
" /help \u2014 Show this help",
|
|
347
|
+
" /clear \u2014 Clear conversation",
|
|
348
|
+
" /compact [instr] \u2014 Compact context (optional focus instructions)",
|
|
349
|
+
" /mode [m] \u2014 Show/change permission mode",
|
|
350
|
+
" /language [lang] \u2014 Set response language (ko, en, ja, zh)",
|
|
351
|
+
" /cost \u2014 Show session info",
|
|
352
|
+
" /reset \u2014 Delete settings and exit",
|
|
353
|
+
" /exit \u2014 Exit CLI"
|
|
354
|
+
].join("\n");
|
|
355
|
+
function handleHelp(addMessage) {
|
|
356
|
+
addMessage({ role: "system", content: HELP_TEXT });
|
|
357
|
+
return { handled: true };
|
|
358
|
+
}
|
|
359
|
+
function handleClear(addMessage, clearMessages, session) {
|
|
360
|
+
clearMessages();
|
|
361
|
+
session.clearHistory();
|
|
362
|
+
addMessage({ role: "system", content: "Conversation cleared." });
|
|
363
|
+
return { handled: true };
|
|
364
|
+
}
|
|
365
|
+
async function handleCompact(args, session, addMessage) {
|
|
366
|
+
const instructions = args.trim() || void 0;
|
|
367
|
+
const before = session.getContextState().usedPercentage;
|
|
368
|
+
addMessage({ role: "system", content: "Compacting context..." });
|
|
369
|
+
await session.compact(instructions);
|
|
370
|
+
const after = session.getContextState().usedPercentage;
|
|
371
|
+
addMessage({
|
|
372
|
+
role: "system",
|
|
373
|
+
content: `Context compacted: ${Math.round(before)}% -> ${Math.round(after)}%`
|
|
374
|
+
});
|
|
375
|
+
return { handled: true };
|
|
376
|
+
}
|
|
377
|
+
function handleMode(arg, session, addMessage) {
|
|
378
|
+
if (!arg) {
|
|
379
|
+
addMessage({ role: "system", content: `Current mode: ${session.getPermissionMode()}` });
|
|
380
|
+
} else if (VALID_MODES2.includes(arg)) {
|
|
381
|
+
session.setPermissionMode(arg);
|
|
382
|
+
addMessage({ role: "system", content: `Permission mode set to: ${arg}` });
|
|
383
|
+
} else {
|
|
384
|
+
addMessage({ role: "system", content: `Invalid mode. Valid: ${VALID_MODES2.join(" | ")}` });
|
|
385
|
+
}
|
|
386
|
+
return { handled: true };
|
|
387
|
+
}
|
|
388
|
+
function handleModel(modelId, addMessage) {
|
|
389
|
+
if (!modelId) {
|
|
390
|
+
addMessage({ role: "system", content: "Select a model from the /model submenu." });
|
|
391
|
+
return { handled: true };
|
|
392
|
+
}
|
|
393
|
+
return { handled: true, pendingModelId: modelId };
|
|
394
|
+
}
|
|
395
|
+
function handleCost(session, addMessage) {
|
|
396
|
+
addMessage({
|
|
397
|
+
role: "system",
|
|
398
|
+
content: `Session: ${session.getSessionId()}
|
|
399
|
+
Messages: ${session.getMessageCount()}`
|
|
400
|
+
});
|
|
401
|
+
return { handled: true };
|
|
402
|
+
}
|
|
403
|
+
function handlePermissions(session, addMessage) {
|
|
404
|
+
const mode = session.getPermissionMode();
|
|
405
|
+
const sessionAllowed = session.getSessionAllowedTools();
|
|
406
|
+
const lines = [`Permission mode: ${mode}`];
|
|
407
|
+
if (sessionAllowed.length > 0) {
|
|
408
|
+
lines.push(`Session-approved tools: ${sessionAllowed.join(", ")}`);
|
|
409
|
+
} else {
|
|
410
|
+
lines.push("No session-approved tools.");
|
|
411
|
+
}
|
|
412
|
+
addMessage({ role: "system", content: lines.join("\n") });
|
|
413
|
+
return { handled: true };
|
|
414
|
+
}
|
|
415
|
+
function handleContext(session, addMessage) {
|
|
416
|
+
const ctx = session.getContextState();
|
|
417
|
+
addMessage({
|
|
418
|
+
role: "system",
|
|
419
|
+
content: `Context: ${ctx.usedTokens.toLocaleString()} / ${ctx.maxTokens.toLocaleString()} tokens (${Math.round(ctx.usedPercentage)}%)`
|
|
420
|
+
});
|
|
421
|
+
return { handled: true };
|
|
422
|
+
}
|
|
423
|
+
function handleLanguage(lang, addMessage) {
|
|
424
|
+
if (!lang) {
|
|
425
|
+
addMessage({ role: "system", content: "Usage: /language <code> (e.g., ko, en, ja, zh)" });
|
|
426
|
+
return { handled: true };
|
|
427
|
+
}
|
|
428
|
+
const settingsPath = getUserSettingsPath();
|
|
429
|
+
const settings = readSettings(settingsPath);
|
|
430
|
+
settings.language = lang;
|
|
431
|
+
writeSettings(settingsPath, settings);
|
|
432
|
+
addMessage({ role: "system", content: `Language set to "${lang}". Restarting...` });
|
|
433
|
+
return { handled: true, exitRequested: true };
|
|
434
|
+
}
|
|
435
|
+
function handleReset(addMessage) {
|
|
436
|
+
const settingsPath = getUserSettingsPath();
|
|
437
|
+
if (deleteSettings(settingsPath)) {
|
|
438
|
+
addMessage({ role: "system", content: `Deleted ${settingsPath}. Exiting...` });
|
|
439
|
+
} else {
|
|
440
|
+
addMessage({ role: "system", content: "No user settings found." });
|
|
441
|
+
}
|
|
442
|
+
return { handled: true, exitRequested: true };
|
|
443
|
+
}
|
|
444
|
+
async function handlePluginCommand(args, addMessage, callbacks) {
|
|
445
|
+
const parts = args.trim().split(/\s+/);
|
|
446
|
+
const subcommand = parts[0] ?? "";
|
|
447
|
+
const subArgs = parts.slice(1).join(" ").trim();
|
|
448
|
+
try {
|
|
449
|
+
switch (subcommand) {
|
|
450
|
+
case "":
|
|
451
|
+
case void 0: {
|
|
452
|
+
const plugins = await callbacks.listInstalled();
|
|
453
|
+
if (plugins.length === 0) {
|
|
454
|
+
addMessage({ role: "system", content: "No plugins installed." });
|
|
455
|
+
} else {
|
|
456
|
+
const lines = plugins.map(
|
|
457
|
+
(p) => ` ${p.name} \u2014 ${p.description} [${p.enabled ? "enabled" : "disabled"}]`
|
|
458
|
+
);
|
|
459
|
+
addMessage({ role: "system", content: `Installed plugins:
|
|
460
|
+
${lines.join("\n")}` });
|
|
461
|
+
}
|
|
462
|
+
return { handled: true };
|
|
463
|
+
}
|
|
464
|
+
case "install": {
|
|
465
|
+
if (!subArgs) {
|
|
466
|
+
addMessage({ role: "system", content: "Usage: /plugin install <name>@<marketplace>" });
|
|
467
|
+
return { handled: true };
|
|
468
|
+
}
|
|
469
|
+
await callbacks.install(subArgs);
|
|
470
|
+
addMessage({ role: "system", content: `Installed plugin: ${subArgs}` });
|
|
471
|
+
return { handled: true };
|
|
472
|
+
}
|
|
473
|
+
case "uninstall": {
|
|
474
|
+
if (!subArgs) {
|
|
475
|
+
addMessage({ role: "system", content: "Usage: /plugin uninstall <name>@<marketplace>" });
|
|
476
|
+
return { handled: true };
|
|
477
|
+
}
|
|
478
|
+
await callbacks.uninstall(subArgs);
|
|
479
|
+
addMessage({ role: "system", content: `Uninstalled plugin: ${subArgs}` });
|
|
480
|
+
return { handled: true };
|
|
481
|
+
}
|
|
482
|
+
case "enable": {
|
|
483
|
+
if (!subArgs) {
|
|
484
|
+
addMessage({ role: "system", content: "Usage: /plugin enable <name>@<marketplace>" });
|
|
485
|
+
return { handled: true };
|
|
486
|
+
}
|
|
487
|
+
await callbacks.enable(subArgs);
|
|
488
|
+
addMessage({ role: "system", content: `Enabled plugin: ${subArgs}` });
|
|
489
|
+
return { handled: true };
|
|
490
|
+
}
|
|
491
|
+
case "disable": {
|
|
492
|
+
if (!subArgs) {
|
|
493
|
+
addMessage({ role: "system", content: "Usage: /plugin disable <name>@<marketplace>" });
|
|
494
|
+
return { handled: true };
|
|
495
|
+
}
|
|
496
|
+
await callbacks.disable(subArgs);
|
|
497
|
+
addMessage({ role: "system", content: `Disabled plugin: ${subArgs}` });
|
|
498
|
+
return { handled: true };
|
|
499
|
+
}
|
|
500
|
+
case "marketplace": {
|
|
501
|
+
const mpParts = subArgs.split(/\s+/);
|
|
502
|
+
const mpSubcommand = mpParts[0] ?? "";
|
|
503
|
+
const mpArgs = mpParts.slice(1).join(" ").trim();
|
|
504
|
+
if (mpSubcommand === "add" && mpArgs) {
|
|
505
|
+
const registeredName = await callbacks.marketplaceAdd(mpArgs);
|
|
506
|
+
addMessage({
|
|
507
|
+
role: "system",
|
|
508
|
+
content: `Added marketplace: "${registeredName}" (from ${mpArgs})
|
|
509
|
+
Install plugins with: /plugin install <name>@${registeredName}`
|
|
510
|
+
});
|
|
511
|
+
return { handled: true };
|
|
512
|
+
} else if (mpSubcommand === "remove" && mpArgs) {
|
|
513
|
+
await callbacks.marketplaceRemove(mpArgs);
|
|
514
|
+
addMessage({
|
|
515
|
+
role: "system",
|
|
516
|
+
content: `Removed marketplace "${mpArgs}" and uninstalled its plugins.`
|
|
517
|
+
});
|
|
518
|
+
return { handled: true };
|
|
519
|
+
} else if (mpSubcommand === "update" && mpArgs) {
|
|
520
|
+
await callbacks.marketplaceUpdate(mpArgs);
|
|
521
|
+
addMessage({
|
|
522
|
+
role: "system",
|
|
523
|
+
content: `Updated marketplace "${mpArgs}".`
|
|
524
|
+
});
|
|
525
|
+
return { handled: true };
|
|
526
|
+
} else if (mpSubcommand === "list") {
|
|
527
|
+
const sources = await callbacks.marketplaceList();
|
|
528
|
+
if (sources.length === 0) {
|
|
529
|
+
addMessage({ role: "system", content: "No marketplace sources configured." });
|
|
530
|
+
} else {
|
|
531
|
+
const lines = sources.map((s) => ` ${s.name} (${s.type})`);
|
|
532
|
+
addMessage({ role: "system", content: `Marketplace sources:
|
|
533
|
+
${lines.join("\n")}` });
|
|
534
|
+
}
|
|
535
|
+
return { handled: true };
|
|
536
|
+
} else {
|
|
537
|
+
addMessage({
|
|
538
|
+
role: "system",
|
|
539
|
+
content: "Usage: /plugin marketplace add <source> | remove <name> | update <name> | list"
|
|
540
|
+
});
|
|
541
|
+
return { handled: true };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
default:
|
|
545
|
+
addMessage({ role: "system", content: `Unknown plugin subcommand: ${subcommand}` });
|
|
546
|
+
return { handled: true };
|
|
547
|
+
}
|
|
548
|
+
} catch (error) {
|
|
549
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
550
|
+
addMessage({ role: "system", content: `Plugin error: ${message}` });
|
|
551
|
+
return { handled: true };
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async function handleReloadPlugins(addMessage, callbacks) {
|
|
555
|
+
await callbacks.reloadPlugins();
|
|
556
|
+
addMessage({ role: "system", content: "Plugins reload complete." });
|
|
557
|
+
return { handled: true };
|
|
558
|
+
}
|
|
559
|
+
async function executeSlashCommand(cmd, args, session, addMessage, clearMessages, registry, pluginCallbacks) {
|
|
560
|
+
switch (cmd) {
|
|
561
|
+
case "help":
|
|
562
|
+
return handleHelp(addMessage);
|
|
563
|
+
case "clear":
|
|
564
|
+
return handleClear(addMessage, clearMessages, session);
|
|
565
|
+
case "compact":
|
|
566
|
+
return handleCompact(args, session, addMessage);
|
|
567
|
+
case "mode":
|
|
568
|
+
return handleMode(args.split(/\s+/)[0] || void 0, session, addMessage);
|
|
569
|
+
case "model":
|
|
570
|
+
return handleModel(args.split(/\s+/)[0] || void 0, addMessage);
|
|
571
|
+
case "language":
|
|
572
|
+
return handleLanguage(args.split(/\s+/)[0] || void 0, addMessage);
|
|
573
|
+
case "cost":
|
|
574
|
+
return handleCost(session, addMessage);
|
|
575
|
+
case "permissions":
|
|
576
|
+
return handlePermissions(session, addMessage);
|
|
577
|
+
case "context":
|
|
578
|
+
return handleContext(session, addMessage);
|
|
579
|
+
case "reset":
|
|
580
|
+
return handleReset(addMessage);
|
|
581
|
+
case "exit":
|
|
582
|
+
return { handled: true, exitRequested: true };
|
|
583
|
+
case "plugin":
|
|
584
|
+
if (pluginCallbacks) {
|
|
585
|
+
return handlePluginCommand(args, addMessage, pluginCallbacks);
|
|
586
|
+
}
|
|
587
|
+
addMessage({ role: "system", content: "Plugin management is not available." });
|
|
588
|
+
return { handled: true };
|
|
589
|
+
case "reload-plugins":
|
|
590
|
+
if (pluginCallbacks) {
|
|
591
|
+
return handleReloadPlugins(addMessage, pluginCallbacks);
|
|
592
|
+
}
|
|
593
|
+
addMessage({ role: "system", content: "Plugin management is not available." });
|
|
594
|
+
return { handled: true };
|
|
595
|
+
default: {
|
|
596
|
+
const dynamicCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
|
|
597
|
+
if (dynamicCmd) {
|
|
598
|
+
addMessage({ role: "system", content: `Invoking ${dynamicCmd.source}: ${cmd}` });
|
|
599
|
+
return { handled: false };
|
|
600
|
+
}
|
|
601
|
+
addMessage({ role: "system", content: `Unknown command "/${cmd}". Type /help for help.` });
|
|
602
|
+
return { handled: true };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/ui/hooks/useSlashCommands.ts
|
|
608
|
+
var EXIT_DELAY_MS = 500;
|
|
609
|
+
function useSlashCommands(session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId, pluginCallbacks) {
|
|
610
|
+
return useCallback3(
|
|
611
|
+
async (input) => {
|
|
612
|
+
const parts = input.slice(1).split(/\s+/);
|
|
613
|
+
const cmd = parts[0]?.toLowerCase() ?? "";
|
|
614
|
+
const args = parts.slice(1).join(" ");
|
|
615
|
+
const clearMessages = () => setMessages([]);
|
|
616
|
+
const result = await executeSlashCommand(
|
|
617
|
+
cmd,
|
|
618
|
+
args,
|
|
619
|
+
session,
|
|
620
|
+
addMessage,
|
|
621
|
+
clearMessages,
|
|
622
|
+
registry,
|
|
623
|
+
pluginCallbacks
|
|
624
|
+
);
|
|
625
|
+
if (result.pendingModelId) {
|
|
626
|
+
pendingModelChangeRef.current = result.pendingModelId;
|
|
627
|
+
setPendingModelId(result.pendingModelId);
|
|
628
|
+
}
|
|
629
|
+
if (result.exitRequested) {
|
|
630
|
+
setTimeout(() => exit(), EXIT_DELAY_MS);
|
|
631
|
+
}
|
|
632
|
+
return result.handled;
|
|
633
|
+
},
|
|
634
|
+
[
|
|
635
|
+
session,
|
|
636
|
+
addMessage,
|
|
637
|
+
setMessages,
|
|
638
|
+
exit,
|
|
639
|
+
registry,
|
|
640
|
+
pendingModelChangeRef,
|
|
641
|
+
setPendingModelId,
|
|
642
|
+
pluginCallbacks
|
|
643
|
+
]
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/ui/hooks/useSubmitHandler.ts
|
|
648
|
+
import { useCallback as useCallback4 } from "react";
|
|
649
|
+
|
|
650
|
+
// src/utils/tool-call-extractor.ts
|
|
651
|
+
var TOOL_ARG_MAX_LENGTH = 80;
|
|
652
|
+
var TAIL_KEEP2 = 30;
|
|
653
|
+
function extractToolCallsWithDiff(history, startIndex) {
|
|
654
|
+
const summaries = [];
|
|
655
|
+
for (let i = startIndex; i < history.length; i++) {
|
|
656
|
+
const msg = history[i];
|
|
657
|
+
if (msg.role === "assistant" && msg.toolCalls) {
|
|
658
|
+
for (const tc of msg.toolCalls) {
|
|
659
|
+
const value = parseFirstArgValue(tc.function.arguments);
|
|
660
|
+
const truncated = value.length > TOOL_ARG_MAX_LENGTH ? value.slice(0, TOOL_ARG_MAX_LENGTH - TAIL_KEEP2 - 3) + "..." + value.slice(-TAIL_KEEP2) : value;
|
|
661
|
+
const summary = {
|
|
662
|
+
line: `${tc.function.name}(${truncated})`
|
|
663
|
+
};
|
|
664
|
+
if (tc.function.name === "Edit") {
|
|
665
|
+
try {
|
|
666
|
+
const args = JSON.parse(tc.function.arguments);
|
|
667
|
+
const diff = extractEditDiff("Edit", args);
|
|
668
|
+
if (diff) {
|
|
669
|
+
summary.diffLines = diff.lines;
|
|
670
|
+
summary.diffFile = diff.file;
|
|
671
|
+
}
|
|
672
|
+
} catch {
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
summaries.push(summary);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return summaries;
|
|
680
|
+
}
|
|
681
|
+
function parseFirstArgValue(argsJson) {
|
|
682
|
+
try {
|
|
683
|
+
const parsed = JSON.parse(argsJson);
|
|
684
|
+
const firstVal = Object.values(parsed)[0];
|
|
685
|
+
return typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal);
|
|
686
|
+
} catch {
|
|
687
|
+
return argsJson;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/utils/skill-prompt.ts
|
|
692
|
+
import { execSync } from "child_process";
|
|
693
|
+
function substituteVariables(content, args, context) {
|
|
694
|
+
const argParts = args ? args.split(/\s+/) : [];
|
|
695
|
+
let result = content;
|
|
696
|
+
result = result.replace(/\$ARGUMENTS\[(\d+)]/g, (_match, index) => {
|
|
697
|
+
return argParts[Number(index)] ?? "";
|
|
698
|
+
});
|
|
699
|
+
result = result.replace(/\$ARGUMENTS/g, args);
|
|
700
|
+
result = result.replace(/\$(\d)(?!\d|\w|\[)/g, (_match, digit) => {
|
|
701
|
+
return argParts[Number(digit)] ?? "";
|
|
702
|
+
});
|
|
703
|
+
result = result.replace(/\$\{CLAUDE_SESSION_ID}/g, context?.sessionId ?? "");
|
|
704
|
+
result = result.replace(/\$\{CLAUDE_SKILL_DIR}/g, context?.skillDir ?? "");
|
|
705
|
+
return result;
|
|
706
|
+
}
|
|
707
|
+
async function preprocessShellCommands(content) {
|
|
708
|
+
const shellPattern = /!`([^`]+)`/g;
|
|
709
|
+
if (!shellPattern.test(content)) {
|
|
710
|
+
return content;
|
|
711
|
+
}
|
|
712
|
+
shellPattern.lastIndex = 0;
|
|
713
|
+
let result = content;
|
|
714
|
+
let match;
|
|
715
|
+
const matches = [];
|
|
716
|
+
while ((match = shellPattern.exec(content)) !== null) {
|
|
717
|
+
matches.push({ full: match[0], command: match[1] });
|
|
718
|
+
}
|
|
719
|
+
for (const { full, command } of matches) {
|
|
720
|
+
let output = "";
|
|
721
|
+
try {
|
|
722
|
+
output = execSync(command, {
|
|
723
|
+
timeout: 5e3,
|
|
724
|
+
encoding: "utf-8",
|
|
725
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
726
|
+
}).trimEnd();
|
|
727
|
+
} catch {
|
|
728
|
+
output = "";
|
|
729
|
+
}
|
|
730
|
+
result = result.replace(full, output);
|
|
731
|
+
}
|
|
732
|
+
return result;
|
|
733
|
+
}
|
|
734
|
+
async function buildSkillPrompt(input, registry, context) {
|
|
735
|
+
const parts = input.slice(1).split(/\s+/);
|
|
736
|
+
const cmd = parts[0]?.toLowerCase() ?? "";
|
|
737
|
+
const skillCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
|
|
738
|
+
if (!skillCmd) return null;
|
|
739
|
+
const args = parts.slice(1).join(" ").trim();
|
|
740
|
+
const userInstruction = args || skillCmd.description;
|
|
741
|
+
if (skillCmd.skillContent) {
|
|
742
|
+
let processed = await preprocessShellCommands(skillCmd.skillContent);
|
|
743
|
+
processed = substituteVariables(processed, args, context);
|
|
744
|
+
return `<skill name="${cmd}">
|
|
745
|
+
${processed}
|
|
746
|
+
</skill>
|
|
747
|
+
|
|
748
|
+
Execute the "${cmd}" skill: ${userInstruction}`;
|
|
749
|
+
}
|
|
750
|
+
return `Use the "${cmd}" skill: ${userInstruction}`;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/ui/hooks/useSubmitHandler.ts
|
|
754
|
+
function syncContextState(session, setter) {
|
|
755
|
+
const ctx = session.getContextState();
|
|
756
|
+
setter({ percentage: ctx.usedPercentage, usedTokens: ctx.usedTokens, maxTokens: ctx.maxTokens });
|
|
757
|
+
}
|
|
758
|
+
async function runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextState, rawInput) {
|
|
759
|
+
setIsThinking(true);
|
|
760
|
+
clearStreamingText();
|
|
761
|
+
const historyBefore = session.getHistory().length;
|
|
762
|
+
try {
|
|
763
|
+
const response = await session.run(prompt, rawInput);
|
|
764
|
+
clearStreamingText();
|
|
765
|
+
const history = session.getHistory();
|
|
766
|
+
const toolSummaries = extractToolCallsWithDiff(
|
|
767
|
+
history,
|
|
768
|
+
historyBefore
|
|
769
|
+
);
|
|
770
|
+
if (toolSummaries.length > 0) {
|
|
771
|
+
addMessage({
|
|
772
|
+
role: "tool",
|
|
773
|
+
content: JSON.stringify(toolSummaries),
|
|
774
|
+
toolName: `${toolSummaries.length} tools`
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
addMessage({ role: "assistant", content: response || "(empty response)" });
|
|
778
|
+
syncContextState(session, setContextState);
|
|
779
|
+
} catch (err) {
|
|
780
|
+
clearStreamingText();
|
|
781
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
782
|
+
addMessage({ role: "system", content: "Cancelled." });
|
|
783
|
+
} else {
|
|
784
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
785
|
+
addMessage({ role: "system", content: `Error: ${errMsg}` });
|
|
786
|
+
}
|
|
787
|
+
} finally {
|
|
788
|
+
setIsThinking(false);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamingText, setIsThinking, setContextState, registry) {
|
|
792
|
+
return useCallback4(
|
|
793
|
+
async (input) => {
|
|
794
|
+
if (input.startsWith("/")) {
|
|
795
|
+
const handled = await handleSlashCommand(input);
|
|
796
|
+
if (handled) {
|
|
797
|
+
syncContextState(session, setContextState);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const prompt = await buildSkillPrompt(input, registry);
|
|
801
|
+
if (!prompt) return;
|
|
802
|
+
const cmdName = input.slice(1).split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
803
|
+
const qualifiedName = registry.resolveQualifiedName(cmdName);
|
|
804
|
+
const hookInput = qualifiedName ? `/${qualifiedName}${input.slice(1 + cmdName.length)}` : input;
|
|
805
|
+
return runSessionPrompt(
|
|
806
|
+
prompt,
|
|
807
|
+
session,
|
|
808
|
+
addMessage,
|
|
809
|
+
clearStreamingText,
|
|
810
|
+
setIsThinking,
|
|
811
|
+
setContextState,
|
|
812
|
+
hookInput
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
addMessage({ role: "user", content: input });
|
|
816
|
+
return runSessionPrompt(
|
|
817
|
+
input,
|
|
818
|
+
session,
|
|
819
|
+
addMessage,
|
|
820
|
+
clearStreamingText,
|
|
821
|
+
setIsThinking,
|
|
822
|
+
setContextState
|
|
823
|
+
);
|
|
824
|
+
},
|
|
825
|
+
[
|
|
826
|
+
session,
|
|
827
|
+
addMessage,
|
|
828
|
+
handleSlashCommand,
|
|
829
|
+
clearStreamingText,
|
|
830
|
+
setIsThinking,
|
|
831
|
+
setContextState,
|
|
832
|
+
registry
|
|
833
|
+
]
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/ui/hooks/useCommandRegistry.ts
|
|
838
|
+
import { useRef as useRef2 } from "react";
|
|
839
|
+
import { homedir as homedir2 } from "os";
|
|
840
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
841
|
+
import { BundlePluginLoader } from "@robota-sdk/agent-sdk";
|
|
842
|
+
|
|
843
|
+
// src/commands/command-registry.ts
|
|
844
|
+
var CommandRegistry = class {
|
|
845
|
+
sources = [];
|
|
846
|
+
addSource(source) {
|
|
847
|
+
this.sources.push(source);
|
|
848
|
+
}
|
|
849
|
+
/** Get all commands, optionally filtered by prefix */
|
|
850
|
+
getCommands(filter) {
|
|
851
|
+
const all = [];
|
|
852
|
+
for (const source of this.sources) {
|
|
853
|
+
all.push(...source.getCommands());
|
|
854
|
+
}
|
|
855
|
+
if (!filter) return all;
|
|
856
|
+
const lower = filter.toLowerCase();
|
|
857
|
+
return all.filter((cmd) => cmd.name.toLowerCase().startsWith(lower));
|
|
858
|
+
}
|
|
859
|
+
/** Resolve a short name to its fully qualified plugin:name form */
|
|
860
|
+
resolveQualifiedName(shortName) {
|
|
861
|
+
const matches = this.getCommands().filter(
|
|
862
|
+
(c) => c.source === "plugin" && c.name.includes(":") && c.name.endsWith(`:${shortName}`)
|
|
863
|
+
);
|
|
864
|
+
if (matches.length !== 1) return null;
|
|
865
|
+
return matches[0].name;
|
|
866
|
+
}
|
|
867
|
+
/** Get subcommands for a specific command */
|
|
868
|
+
getSubcommands(commandName) {
|
|
869
|
+
const lower = commandName.toLowerCase();
|
|
870
|
+
for (const source of this.sources) {
|
|
871
|
+
for (const cmd of source.getCommands()) {
|
|
872
|
+
if (cmd.name.toLowerCase() === lower && cmd.subcommands) {
|
|
873
|
+
return cmd.subcommands;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return [];
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
// src/commands/builtin-source.ts
|
|
882
|
+
import { CLAUDE_MODELS, formatTokenCount } from "@robota-sdk/agent-core";
|
|
883
|
+
function buildModelSubcommands() {
|
|
884
|
+
const seen = /* @__PURE__ */ new Set();
|
|
885
|
+
const commands = [];
|
|
886
|
+
for (const model of Object.values(CLAUDE_MODELS)) {
|
|
887
|
+
if (seen.has(model.name)) continue;
|
|
888
|
+
seen.add(model.name);
|
|
889
|
+
commands.push({
|
|
890
|
+
name: model.id,
|
|
891
|
+
description: `${model.name} (${formatTokenCount(model.contextWindow).toUpperCase()})`,
|
|
892
|
+
source: "builtin"
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
return commands;
|
|
896
|
+
}
|
|
897
|
+
function createBuiltinCommands() {
|
|
898
|
+
return [
|
|
899
|
+
{ name: "help", description: "Show available commands", source: "builtin" },
|
|
900
|
+
{ name: "clear", description: "Clear conversation history", source: "builtin" },
|
|
901
|
+
{
|
|
902
|
+
name: "mode",
|
|
903
|
+
description: "Permission mode",
|
|
904
|
+
source: "builtin",
|
|
905
|
+
subcommands: [
|
|
906
|
+
{ name: "plan", description: "Plan only, no execution", source: "builtin" },
|
|
907
|
+
{ name: "default", description: "Ask before risky actions", source: "builtin" },
|
|
908
|
+
{ name: "acceptEdits", description: "Auto-approve file edits", source: "builtin" },
|
|
909
|
+
{ name: "bypassPermissions", description: "Skip all permission checks", source: "builtin" }
|
|
910
|
+
]
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
name: "model",
|
|
914
|
+
description: "Select AI model",
|
|
915
|
+
source: "builtin",
|
|
916
|
+
subcommands: buildModelSubcommands()
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
name: "language",
|
|
920
|
+
description: "Set response language",
|
|
921
|
+
source: "builtin",
|
|
922
|
+
subcommands: [
|
|
923
|
+
{ name: "ko", description: "Korean", source: "builtin" },
|
|
924
|
+
{ name: "en", description: "English", source: "builtin" },
|
|
925
|
+
{ name: "ja", description: "Japanese", source: "builtin" },
|
|
926
|
+
{ name: "zh", description: "Chinese", source: "builtin" }
|
|
927
|
+
]
|
|
928
|
+
},
|
|
929
|
+
{ name: "compact", description: "Compress context window", source: "builtin" },
|
|
930
|
+
{ name: "cost", description: "Show session info", source: "builtin" },
|
|
931
|
+
{ name: "context", description: "Context window info", source: "builtin" },
|
|
932
|
+
{ name: "permissions", description: "Permission rules", source: "builtin" },
|
|
933
|
+
{
|
|
934
|
+
name: "plugin",
|
|
935
|
+
description: "Manage plugins",
|
|
936
|
+
source: "builtin",
|
|
937
|
+
subcommands: [
|
|
938
|
+
{ name: "install", description: "Install a plugin (name@marketplace)", source: "builtin" },
|
|
939
|
+
{
|
|
940
|
+
name: "uninstall",
|
|
941
|
+
description: "Uninstall a plugin (name@marketplace)",
|
|
942
|
+
source: "builtin"
|
|
943
|
+
},
|
|
944
|
+
{ name: "enable", description: "Enable a plugin (name@marketplace)", source: "builtin" },
|
|
945
|
+
{ name: "disable", description: "Disable a plugin (name@marketplace)", source: "builtin" },
|
|
946
|
+
{ name: "marketplace", description: "Manage marketplace sources", source: "builtin" }
|
|
947
|
+
]
|
|
948
|
+
},
|
|
949
|
+
{ name: "reload-plugins", description: "Reload all plugin resources", source: "builtin" },
|
|
950
|
+
{ name: "reset", description: "Delete settings and exit", source: "builtin" },
|
|
951
|
+
{ name: "exit", description: "Exit CLI", source: "builtin" }
|
|
952
|
+
];
|
|
953
|
+
}
|
|
954
|
+
var BuiltinCommandSource = class {
|
|
955
|
+
name = "builtin";
|
|
956
|
+
commands;
|
|
957
|
+
constructor() {
|
|
958
|
+
this.commands = createBuiltinCommands();
|
|
959
|
+
}
|
|
960
|
+
getCommands() {
|
|
961
|
+
return this.commands;
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// src/commands/skill-source.ts
|
|
966
|
+
import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
967
|
+
import { join as join2, basename } from "path";
|
|
968
|
+
import { homedir } from "os";
|
|
969
|
+
var BOOLEAN_KEYS = /* @__PURE__ */ new Set(["disable-model-invocation", "user-invocable"]);
|
|
970
|
+
var LIST_KEYS = /* @__PURE__ */ new Set(["allowed-tools"]);
|
|
971
|
+
function kebabToCamel(key) {
|
|
972
|
+
return key.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
973
|
+
}
|
|
974
|
+
function parseFrontmatter(content) {
|
|
975
|
+
const lines = content.split("\n");
|
|
976
|
+
if (lines[0]?.trim() !== "---") return null;
|
|
977
|
+
const result = {};
|
|
978
|
+
for (let i = 1; i < lines.length; i++) {
|
|
979
|
+
const line = lines[i];
|
|
980
|
+
if (line.trim() === "---") break;
|
|
981
|
+
const match = line.match(/^([a-z][a-z0-9-]*):\s*(.+)/);
|
|
982
|
+
if (!match) continue;
|
|
983
|
+
const key = match[1];
|
|
984
|
+
const rawValue = match[2].trim();
|
|
985
|
+
const camelKey = kebabToCamel(key);
|
|
986
|
+
if (BOOLEAN_KEYS.has(key)) {
|
|
987
|
+
result[camelKey] = rawValue === "true";
|
|
988
|
+
} else if (LIST_KEYS.has(key)) {
|
|
989
|
+
result[camelKey] = rawValue.split(",").map((s) => s.trim());
|
|
990
|
+
} else {
|
|
991
|
+
result[camelKey] = rawValue;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
995
|
+
}
|
|
996
|
+
function buildCommand(frontmatter, content, fallbackName) {
|
|
997
|
+
const cmd = {
|
|
998
|
+
name: frontmatter?.name ?? fallbackName,
|
|
999
|
+
description: frontmatter?.description ?? `Skill: ${fallbackName}`,
|
|
1000
|
+
source: "skill",
|
|
1001
|
+
skillContent: content
|
|
1002
|
+
};
|
|
1003
|
+
if (frontmatter?.argumentHint !== void 0) cmd.argumentHint = frontmatter.argumentHint;
|
|
1004
|
+
if (frontmatter?.disableModelInvocation !== void 0)
|
|
1005
|
+
cmd.disableModelInvocation = frontmatter.disableModelInvocation;
|
|
1006
|
+
if (frontmatter?.userInvocable !== void 0) cmd.userInvocable = frontmatter.userInvocable;
|
|
1007
|
+
if (frontmatter?.allowedTools !== void 0) cmd.allowedTools = frontmatter.allowedTools;
|
|
1008
|
+
if (frontmatter?.model !== void 0) cmd.model = frontmatter.model;
|
|
1009
|
+
if (frontmatter?.effort !== void 0) cmd.effort = frontmatter.effort;
|
|
1010
|
+
if (frontmatter?.context !== void 0) cmd.context = frontmatter.context;
|
|
1011
|
+
if (frontmatter?.agent !== void 0) cmd.agent = frontmatter.agent;
|
|
1012
|
+
return cmd;
|
|
1013
|
+
}
|
|
1014
|
+
function scanSkillsDir(skillsDir) {
|
|
1015
|
+
if (!existsSync2(skillsDir)) return [];
|
|
1016
|
+
const commands = [];
|
|
1017
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
1018
|
+
for (const entry of entries) {
|
|
1019
|
+
if (!entry.isDirectory()) continue;
|
|
1020
|
+
const skillFile = join2(skillsDir, entry.name, "SKILL.md");
|
|
1021
|
+
if (!existsSync2(skillFile)) continue;
|
|
1022
|
+
const content = readFileSync2(skillFile, "utf-8");
|
|
1023
|
+
const frontmatter = parseFrontmatter(content);
|
|
1024
|
+
commands.push(buildCommand(frontmatter, content, entry.name));
|
|
1025
|
+
}
|
|
1026
|
+
return commands;
|
|
1027
|
+
}
|
|
1028
|
+
function scanCommandsDir(commandsDir) {
|
|
1029
|
+
if (!existsSync2(commandsDir)) return [];
|
|
1030
|
+
const commands = [];
|
|
1031
|
+
const entries = readdirSync(commandsDir, { withFileTypes: true });
|
|
1032
|
+
for (const entry of entries) {
|
|
1033
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1034
|
+
const filePath = join2(commandsDir, entry.name);
|
|
1035
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
1036
|
+
const frontmatter = parseFrontmatter(content);
|
|
1037
|
+
const fallbackName = basename(entry.name, ".md");
|
|
1038
|
+
commands.push(buildCommand(frontmatter, content, fallbackName));
|
|
1039
|
+
}
|
|
1040
|
+
return commands;
|
|
1041
|
+
}
|
|
1042
|
+
var SkillCommandSource = class {
|
|
1043
|
+
name = "skill";
|
|
1044
|
+
cwd;
|
|
1045
|
+
home;
|
|
1046
|
+
cachedCommands = null;
|
|
1047
|
+
constructor(cwd, home) {
|
|
1048
|
+
this.cwd = cwd;
|
|
1049
|
+
this.home = home ?? homedir();
|
|
1050
|
+
}
|
|
1051
|
+
getCommands() {
|
|
1052
|
+
if (this.cachedCommands) return this.cachedCommands;
|
|
1053
|
+
const sources = [
|
|
1054
|
+
scanSkillsDir(join2(this.cwd, ".claude", "skills")),
|
|
1055
|
+
// 1. project .claude/skills
|
|
1056
|
+
scanCommandsDir(join2(this.cwd, ".claude", "commands")),
|
|
1057
|
+
// 2. project .claude/commands (legacy)
|
|
1058
|
+
scanSkillsDir(join2(this.home, ".robota", "skills")),
|
|
1059
|
+
// 3. user ~/.robota/skills
|
|
1060
|
+
scanSkillsDir(join2(this.cwd, ".agents", "skills"))
|
|
1061
|
+
// 4. project .agents/skills
|
|
1062
|
+
];
|
|
1063
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1064
|
+
const merged = [];
|
|
1065
|
+
for (const commands of sources) {
|
|
1066
|
+
for (const cmd of commands) {
|
|
1067
|
+
if (!seen.has(cmd.name)) {
|
|
1068
|
+
seen.add(cmd.name);
|
|
1069
|
+
merged.push(cmd);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
this.cachedCommands = merged;
|
|
1074
|
+
return this.cachedCommands;
|
|
1075
|
+
}
|
|
1076
|
+
/** Get skills that models can invoke (excludes disableModelInvocation: true) */
|
|
1077
|
+
getModelInvocableSkills() {
|
|
1078
|
+
return this.getCommands().filter((cmd) => cmd.disableModelInvocation !== true);
|
|
1079
|
+
}
|
|
1080
|
+
/** Get skills that users can invoke (excludes userInvocable: false) */
|
|
1081
|
+
getUserInvocableSkills() {
|
|
1082
|
+
return this.getCommands().filter((cmd) => cmd.userInvocable !== false);
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
// src/commands/plugin-source.ts
|
|
1087
|
+
var PluginCommandSource = class {
|
|
1088
|
+
name = "plugin";
|
|
1089
|
+
plugins;
|
|
1090
|
+
constructor(plugins) {
|
|
1091
|
+
this.plugins = plugins;
|
|
1092
|
+
}
|
|
1093
|
+
getCommands() {
|
|
1094
|
+
const commands = [];
|
|
1095
|
+
for (const plugin of this.plugins) {
|
|
1096
|
+
for (const skill of plugin.skills) {
|
|
1097
|
+
const baseName = skill.name.includes("@") ? skill.name.split("@")[0] : skill.name;
|
|
1098
|
+
commands.push({
|
|
1099
|
+
name: baseName,
|
|
1100
|
+
description: `(${plugin.manifest.name}) ${skill.description}`,
|
|
1101
|
+
source: "plugin",
|
|
1102
|
+
skillContent: skill.skillContent,
|
|
1103
|
+
pluginDir: plugin.pluginDir
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
for (const cmd of plugin.commands) {
|
|
1107
|
+
commands.push({
|
|
1108
|
+
name: cmd.name,
|
|
1109
|
+
description: cmd.description,
|
|
1110
|
+
source: "plugin",
|
|
1111
|
+
skillContent: cmd.skillContent,
|
|
1112
|
+
pluginDir: plugin.pluginDir
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return commands;
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
// src/ui/hooks/useCommandRegistry.ts
|
|
1121
|
+
function buildPluginEnv(plugin) {
|
|
1122
|
+
const dataDir = join3(dirname2(dirname2(plugin.pluginDir)), "data", plugin.manifest.name);
|
|
1123
|
+
return {
|
|
1124
|
+
CLAUDE_PLUGIN_ROOT: plugin.pluginDir,
|
|
1125
|
+
CLAUDE_PLUGIN_PATH: plugin.pluginDir,
|
|
1126
|
+
CLAUDE_PLUGIN_DATA: dataDir
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function mergePluginHooks(plugins) {
|
|
1130
|
+
const merged = {};
|
|
1131
|
+
for (const plugin of plugins) {
|
|
1132
|
+
const hooksObj = plugin.hooks;
|
|
1133
|
+
if (!hooksObj) continue;
|
|
1134
|
+
const pluginEnv = buildPluginEnv(plugin);
|
|
1135
|
+
const innerHooks = hooksObj.hooks ?? hooksObj;
|
|
1136
|
+
for (const [event, groups] of Object.entries(innerHooks)) {
|
|
1137
|
+
if (!Array.isArray(groups)) continue;
|
|
1138
|
+
if (!merged[event]) merged[event] = [];
|
|
1139
|
+
const resolved = groups.map((group) => {
|
|
1140
|
+
const resolved2 = resolvePluginRoot(group, plugin.pluginDir);
|
|
1141
|
+
if (typeof resolved2 === "object" && resolved2 !== null) {
|
|
1142
|
+
resolved2.env = pluginEnv;
|
|
1143
|
+
}
|
|
1144
|
+
return resolved2;
|
|
1145
|
+
});
|
|
1146
|
+
merged[event].push(...resolved);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return merged;
|
|
1150
|
+
}
|
|
1151
|
+
function resolvePluginRoot(group, pluginDir) {
|
|
1152
|
+
if (typeof group !== "object" || group === null) return group;
|
|
1153
|
+
const obj = group;
|
|
1154
|
+
if (Array.isArray(obj.hooks)) {
|
|
1155
|
+
return {
|
|
1156
|
+
...obj,
|
|
1157
|
+
hooks: obj.hooks.map((h) => {
|
|
1158
|
+
if (typeof h !== "object" || h === null) return h;
|
|
1159
|
+
const hook = h;
|
|
1160
|
+
if (typeof hook.command === "string") {
|
|
1161
|
+
return {
|
|
1162
|
+
...hook,
|
|
1163
|
+
command: hook.command.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pluginDir)
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
return hook;
|
|
1167
|
+
})
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
return group;
|
|
1171
|
+
}
|
|
1172
|
+
function useCommandRegistry(cwd) {
|
|
1173
|
+
const resultRef = useRef2(null);
|
|
1174
|
+
if (resultRef.current === null) {
|
|
1175
|
+
const registry = new CommandRegistry();
|
|
1176
|
+
registry.addSource(new BuiltinCommandSource());
|
|
1177
|
+
registry.addSource(new SkillCommandSource(cwd));
|
|
1178
|
+
let pluginHooks = {};
|
|
1179
|
+
const pluginsDir = join3(homedir2(), ".robota", "plugins");
|
|
1180
|
+
const loader = new BundlePluginLoader(pluginsDir);
|
|
1181
|
+
try {
|
|
1182
|
+
const plugins = loader.loadPluginsSync();
|
|
1183
|
+
if (plugins.length > 0) {
|
|
1184
|
+
registry.addSource(new PluginCommandSource(plugins));
|
|
1185
|
+
pluginHooks = mergePluginHooks(plugins);
|
|
1186
|
+
}
|
|
1187
|
+
} catch {
|
|
1188
|
+
}
|
|
1189
|
+
resultRef.current = { registry, pluginHooks };
|
|
1190
|
+
}
|
|
1191
|
+
return resultRef.current;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/ui/hooks/usePluginCallbacks.ts
|
|
1195
|
+
import { useMemo } from "react";
|
|
1196
|
+
import { homedir as homedir3 } from "os";
|
|
1197
|
+
import { join as join4 } from "path";
|
|
1198
|
+
import {
|
|
1199
|
+
PluginSettingsStore,
|
|
1200
|
+
BundlePluginLoader as BundlePluginLoader2,
|
|
1201
|
+
BundlePluginInstaller,
|
|
1202
|
+
MarketplaceClient
|
|
1203
|
+
} from "@robota-sdk/agent-sdk";
|
|
1204
|
+
function usePluginCallbacks(cwd) {
|
|
1205
|
+
return useMemo(() => {
|
|
1206
|
+
const home = homedir3();
|
|
1207
|
+
const pluginsDir = join4(home, ".robota", "plugins");
|
|
1208
|
+
const userSettingsPath = join4(home, ".robota", "settings.json");
|
|
1209
|
+
const settingsStore = new PluginSettingsStore(userSettingsPath);
|
|
1210
|
+
const marketplace = new MarketplaceClient({ pluginsDir });
|
|
1211
|
+
const installer = new BundlePluginInstaller({
|
|
1212
|
+
pluginsDir,
|
|
1213
|
+
settingsStore,
|
|
1214
|
+
marketplaceClient: marketplace
|
|
1215
|
+
});
|
|
1216
|
+
const loader = new BundlePluginLoader2(pluginsDir);
|
|
1217
|
+
return {
|
|
1218
|
+
listInstalled: async () => {
|
|
1219
|
+
const plugins = await loader.loadAll();
|
|
1220
|
+
return plugins.map((p) => ({
|
|
1221
|
+
name: p.manifest.name,
|
|
1222
|
+
description: p.manifest.description,
|
|
1223
|
+
enabled: true
|
|
1224
|
+
}));
|
|
1225
|
+
},
|
|
1226
|
+
install: async (pluginId) => {
|
|
1227
|
+
const [name, marketplaceName] = pluginId.split("@");
|
|
1228
|
+
if (!name || !marketplaceName) {
|
|
1229
|
+
throw new Error("Plugin ID must be in format: name@marketplace");
|
|
1230
|
+
}
|
|
1231
|
+
await installer.install(name, marketplaceName);
|
|
1232
|
+
},
|
|
1233
|
+
uninstall: async (pluginId) => {
|
|
1234
|
+
await installer.uninstall(pluginId);
|
|
1235
|
+
},
|
|
1236
|
+
enable: async (pluginId) => {
|
|
1237
|
+
await installer.enable(pluginId);
|
|
1238
|
+
},
|
|
1239
|
+
disable: async (pluginId) => {
|
|
1240
|
+
await installer.disable(pluginId);
|
|
1241
|
+
},
|
|
1242
|
+
marketplaceAdd: async (source) => {
|
|
1243
|
+
if (source.includes("/") && !source.includes(":")) {
|
|
1244
|
+
return marketplace.addMarketplace({ type: "github", repo: source });
|
|
1245
|
+
} else {
|
|
1246
|
+
return marketplace.addMarketplace({ type: "git", url: source });
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
marketplaceRemove: async (name) => {
|
|
1250
|
+
const installedFromMarketplace = installer.getPluginsByMarketplace(name);
|
|
1251
|
+
for (const record of installedFromMarketplace) {
|
|
1252
|
+
await installer.uninstall(`${record.pluginName}@${record.marketplace}`);
|
|
1253
|
+
}
|
|
1254
|
+
marketplace.removeMarketplace(name);
|
|
1255
|
+
},
|
|
1256
|
+
marketplaceUpdate: async (name) => {
|
|
1257
|
+
marketplace.updateMarketplace(name);
|
|
1258
|
+
},
|
|
1259
|
+
marketplaceList: async () => {
|
|
1260
|
+
return marketplace.listMarketplaces().map((m) => ({
|
|
1261
|
+
name: m.name,
|
|
1262
|
+
type: m.source.type
|
|
1263
|
+
}));
|
|
1264
|
+
},
|
|
1265
|
+
reloadPlugins: async () => {
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
}, [cwd]);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/ui/MessageList.tsx
|
|
1272
|
+
import React2 from "react";
|
|
1273
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
1274
|
+
|
|
1275
|
+
// src/ui/render-markdown.ts
|
|
1276
|
+
import { marked } from "marked";
|
|
1277
|
+
import TerminalRenderer from "marked-terminal";
|
|
1278
|
+
marked.setOptions({
|
|
1279
|
+
renderer: new TerminalRenderer()
|
|
1280
|
+
});
|
|
1281
|
+
function renderMarkdown(md) {
|
|
1282
|
+
const result = marked.parse(md);
|
|
1283
|
+
return typeof result === "string" ? result.trimEnd() : md;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/ui/DiffBlock.tsx
|
|
1287
|
+
import { Box, Text } from "ink";
|
|
1288
|
+
import { jsxs } from "react/jsx-runtime";
|
|
1289
|
+
var MAX_DIFF_LINES = 10;
|
|
1290
|
+
var TRUNCATED_SHOW = 8;
|
|
1291
|
+
function DiffBlock({ file, lines }) {
|
|
1292
|
+
const truncated = lines.length > MAX_DIFF_LINES;
|
|
1293
|
+
const visible = truncated ? lines.slice(0, TRUNCATED_SHOW) : lines;
|
|
1294
|
+
const remaining = lines.length - TRUNCATED_SHOW;
|
|
1295
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 4, children: [
|
|
1296
|
+
file && /* @__PURE__ */ jsxs(Text, { color: "white", dimColor: true, children: [
|
|
1297
|
+
"\u2502 ",
|
|
1298
|
+
file
|
|
1299
|
+
] }),
|
|
1300
|
+
visible.map((line, i) => /* @__PURE__ */ jsxs(Text, { color: line.type === "remove" ? "red" : "greenBright", children: [
|
|
1301
|
+
"\u2502 ",
|
|
1302
|
+
line.type === "remove" ? "-" : "+",
|
|
1303
|
+
" ",
|
|
1304
|
+
line.text
|
|
1305
|
+
] }, i)),
|
|
1306
|
+
truncated && /* @__PURE__ */ jsxs(Text, { color: "white", dimColor: true, children: [
|
|
1307
|
+
"\u2502 ... and ",
|
|
1308
|
+
remaining,
|
|
1309
|
+
" more lines"
|
|
1310
|
+
] })
|
|
1311
|
+
] });
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/ui/MessageList.tsx
|
|
1315
|
+
import { jsx, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1316
|
+
function RoleLabel({ role }) {
|
|
1317
|
+
switch (role) {
|
|
1318
|
+
case "user":
|
|
1319
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "green", bold: true, children: [
|
|
1320
|
+
"You:",
|
|
1321
|
+
" "
|
|
1322
|
+
] });
|
|
1323
|
+
case "assistant":
|
|
1324
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "cyan", bold: true, children: [
|
|
1325
|
+
"Robota:",
|
|
1326
|
+
" "
|
|
1327
|
+
] });
|
|
1328
|
+
case "system":
|
|
1329
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "yellow", bold: true, children: [
|
|
1330
|
+
"System:",
|
|
1331
|
+
" "
|
|
1332
|
+
] });
|
|
1333
|
+
case "tool":
|
|
1334
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "white", bold: true, children: [
|
|
1335
|
+
"Tool:",
|
|
1336
|
+
" "
|
|
1337
|
+
] });
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
function ToolMessage({ message }) {
|
|
1341
|
+
let summaries = null;
|
|
1342
|
+
try {
|
|
1343
|
+
const parsed = JSON.parse(message.content);
|
|
1344
|
+
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0].line === "string") {
|
|
1345
|
+
summaries = parsed;
|
|
1346
|
+
}
|
|
1347
|
+
} catch {
|
|
1348
|
+
}
|
|
1349
|
+
if (summaries) {
|
|
1350
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
1351
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1352
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "white", bold: true, children: [
|
|
1353
|
+
"Tool:",
|
|
1354
|
+
" "
|
|
1355
|
+
] }),
|
|
1356
|
+
message.toolName && /* @__PURE__ */ jsxs2(Text2, { color: "white", dimColor: true, children: [
|
|
1357
|
+
"[",
|
|
1358
|
+
message.toolName,
|
|
1359
|
+
"]"
|
|
1360
|
+
] })
|
|
1361
|
+
] }),
|
|
1362
|
+
/* @__PURE__ */ jsx(Text2, { children: " " }),
|
|
1363
|
+
summaries.map((s, i) => /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1364
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
|
|
1365
|
+
" ",
|
|
1366
|
+
"\u2713",
|
|
1367
|
+
" ",
|
|
1368
|
+
s.line
|
|
1369
|
+
] }),
|
|
1370
|
+
s.diffLines && s.diffLines.length > 0 && /* @__PURE__ */ jsx(DiffBlock, { file: s.diffFile, lines: s.diffLines })
|
|
1371
|
+
] }, i))
|
|
1372
|
+
] });
|
|
1373
|
+
}
|
|
1374
|
+
const lines = message.content.split("\n").filter((l) => l.trim());
|
|
1375
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
1376
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1377
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "white", bold: true, children: [
|
|
1378
|
+
"Tool:",
|
|
1379
|
+
" "
|
|
1380
|
+
] }),
|
|
1381
|
+
message.toolName && /* @__PURE__ */ jsxs2(Text2, { color: "white", dimColor: true, children: [
|
|
1382
|
+
"[",
|
|
1383
|
+
message.toolName,
|
|
1384
|
+
"]"
|
|
1385
|
+
] })
|
|
1386
|
+
] }),
|
|
1387
|
+
/* @__PURE__ */ jsx(Text2, { children: " " }),
|
|
1388
|
+
lines.map((line, i) => /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
|
|
1389
|
+
" ",
|
|
1390
|
+
"\u2713",
|
|
1391
|
+
" ",
|
|
1392
|
+
line
|
|
1393
|
+
] }, i))
|
|
1394
|
+
] });
|
|
1395
|
+
}
|
|
1396
|
+
var MessageItem = React2.memo(function MessageItem2({
|
|
1397
|
+
message
|
|
1398
|
+
}) {
|
|
1399
|
+
if (message.role === "tool") {
|
|
1400
|
+
return /* @__PURE__ */ jsx(ToolMessage, { message });
|
|
1401
|
+
}
|
|
1402
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
1403
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1404
|
+
/* @__PURE__ */ jsx(RoleLabel, { role: message.role }),
|
|
1405
|
+
message.toolName && /* @__PURE__ */ jsxs2(Text2, { color: "magenta", dimColor: true, children: [
|
|
1406
|
+
"[",
|
|
1407
|
+
message.toolName,
|
|
1408
|
+
"]",
|
|
1409
|
+
" "
|
|
1410
|
+
] })
|
|
1411
|
+
] }),
|
|
1412
|
+
/* @__PURE__ */ jsx(Text2, { children: " " }),
|
|
1413
|
+
/* @__PURE__ */ jsx(Box2, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text2, { wrap: "wrap", children: message.role === "assistant" ? renderMarkdown(message.content) : message.content }) })
|
|
1414
|
+
] });
|
|
1415
|
+
});
|
|
1416
|
+
function MessageList({ messages }) {
|
|
1417
|
+
return /* @__PURE__ */ jsx(Box2, { flexDirection: "column", children: messages.map((msg) => /* @__PURE__ */ jsx(MessageItem, { message: msg }, msg.id)) });
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// src/ui/StatusBar.tsx
|
|
1421
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
1422
|
+
import { formatTokenCount as formatTokenCount2 } from "@robota-sdk/agent-core";
|
|
1423
|
+
import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1424
|
+
var CONTEXT_YELLOW_THRESHOLD = 70;
|
|
1425
|
+
var CONTEXT_RED_THRESHOLD = 90;
|
|
1426
|
+
function getContextColor(percentage) {
|
|
1427
|
+
if (percentage >= CONTEXT_RED_THRESHOLD) return "red";
|
|
1428
|
+
if (percentage >= CONTEXT_YELLOW_THRESHOLD) return "yellow";
|
|
1429
|
+
return "green";
|
|
1430
|
+
}
|
|
1431
|
+
function StatusBar({
|
|
1432
|
+
permissionMode,
|
|
1433
|
+
modelName,
|
|
1434
|
+
sessionId: _sessionId,
|
|
1435
|
+
messageCount,
|
|
1436
|
+
isThinking,
|
|
1437
|
+
contextPercentage,
|
|
1438
|
+
contextUsedTokens,
|
|
1439
|
+
contextMaxTokens
|
|
1440
|
+
}) {
|
|
1441
|
+
const contextColor = getContextColor(contextPercentage);
|
|
1442
|
+
return /* @__PURE__ */ jsxs3(
|
|
1443
|
+
Box3,
|
|
1444
|
+
{
|
|
1445
|
+
borderStyle: "single",
|
|
1446
|
+
borderColor: "gray",
|
|
1447
|
+
paddingLeft: 1,
|
|
1448
|
+
paddingRight: 1,
|
|
1449
|
+
justifyContent: "space-between",
|
|
1450
|
+
children: [
|
|
1451
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1452
|
+
/* @__PURE__ */ jsx2(Text3, { color: "cyan", bold: true, children: "Mode:" }),
|
|
1453
|
+
" ",
|
|
1454
|
+
/* @__PURE__ */ jsx2(Text3, { children: permissionMode }),
|
|
1455
|
+
" | ",
|
|
1456
|
+
/* @__PURE__ */ jsx2(Text3, { dimColor: true, children: modelName }),
|
|
1457
|
+
" | ",
|
|
1458
|
+
/* @__PURE__ */ jsxs3(Text3, { color: contextColor, children: [
|
|
1459
|
+
"Context: ",
|
|
1460
|
+
Math.round(contextPercentage),
|
|
1461
|
+
"% (",
|
|
1462
|
+
formatTokenCount2(contextUsedTokens),
|
|
1463
|
+
"/",
|
|
1464
|
+
formatTokenCount2(contextMaxTokens),
|
|
1465
|
+
")"
|
|
1466
|
+
] })
|
|
1467
|
+
] }),
|
|
1468
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1469
|
+
isThinking && /* @__PURE__ */ jsx2(Text3, { color: "yellow", children: "Thinking... " }),
|
|
1470
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1471
|
+
"msgs: ",
|
|
1472
|
+
messageCount
|
|
1473
|
+
] })
|
|
1474
|
+
] })
|
|
1475
|
+
]
|
|
1476
|
+
}
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// src/ui/InputArea.tsx
|
|
1481
|
+
import React5, { useState as useState5, useCallback as useCallback5, useMemo as useMemo2 } from "react";
|
|
1482
|
+
import { Box as Box5, Text as Text7, useInput as useInput2 } from "ink";
|
|
1483
|
+
|
|
1484
|
+
// src/ui/CjkTextInput.tsx
|
|
1485
|
+
import { useRef as useRef3, useState as useState3 } from "react";
|
|
1486
|
+
import { Text as Text4, useInput } from "ink";
|
|
1487
|
+
import chalk from "chalk";
|
|
1488
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
1489
|
+
function filterPrintable(input) {
|
|
1490
|
+
if (!input || input.length === 0) return "";
|
|
1491
|
+
return input.replace(/[\x00-\x1f\x7f]/g, "");
|
|
1492
|
+
}
|
|
1493
|
+
function insertAtCursor(value, cursor, input) {
|
|
1494
|
+
const next = value.slice(0, cursor) + input + value.slice(cursor);
|
|
1495
|
+
return { value: next, cursor: cursor + input.length };
|
|
1496
|
+
}
|
|
1497
|
+
function CjkTextInput({
|
|
1498
|
+
value,
|
|
1499
|
+
onChange,
|
|
1500
|
+
onSubmit,
|
|
1501
|
+
placeholder = "",
|
|
1502
|
+
focus = true,
|
|
1503
|
+
showCursor = true
|
|
1504
|
+
}) {
|
|
1505
|
+
const valueRef = useRef3(value);
|
|
1506
|
+
const cursorRef = useRef3(value.length);
|
|
1507
|
+
const [, forceRender] = useState3(0);
|
|
1508
|
+
if (value !== valueRef.current) {
|
|
1509
|
+
valueRef.current = value;
|
|
1510
|
+
if (cursorRef.current > value.length) {
|
|
1511
|
+
cursorRef.current = value.length;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
useInput(
|
|
1515
|
+
(input, key) => {
|
|
1516
|
+
try {
|
|
1517
|
+
if (key.upArrow || key.downArrow || key.ctrl && input === "c" || key.tab || key.shift && key.tab) {
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
if (key.return) {
|
|
1521
|
+
onSubmit?.(valueRef.current);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
if (key.leftArrow) {
|
|
1525
|
+
if (cursorRef.current > 0) {
|
|
1526
|
+
cursorRef.current -= 1;
|
|
1527
|
+
forceRender((n) => n + 1);
|
|
1528
|
+
}
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
if (key.rightArrow) {
|
|
1532
|
+
if (cursorRef.current < valueRef.current.length) {
|
|
1533
|
+
cursorRef.current += 1;
|
|
1534
|
+
forceRender((n) => n + 1);
|
|
1535
|
+
}
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
if (key.backspace || key.delete) {
|
|
1539
|
+
if (cursorRef.current > 0) {
|
|
1540
|
+
const v = valueRef.current;
|
|
1541
|
+
const next = v.slice(0, cursorRef.current - 1) + v.slice(cursorRef.current);
|
|
1542
|
+
cursorRef.current -= 1;
|
|
1543
|
+
valueRef.current = next;
|
|
1544
|
+
onChange(next);
|
|
1545
|
+
}
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
const printable = filterPrintable(input);
|
|
1549
|
+
if (printable.length === 0) return;
|
|
1550
|
+
const result = insertAtCursor(valueRef.current, cursorRef.current, printable);
|
|
1551
|
+
cursorRef.current = result.cursor;
|
|
1552
|
+
valueRef.current = result.value;
|
|
1553
|
+
onChange(result.value);
|
|
1554
|
+
} catch {
|
|
1555
|
+
}
|
|
1556
|
+
},
|
|
1557
|
+
{ isActive: focus }
|
|
1558
|
+
);
|
|
1559
|
+
return /* @__PURE__ */ jsx3(Text4, { children: renderWithCursor(valueRef.current, cursorRef.current, placeholder, showCursor && focus) });
|
|
1560
|
+
}
|
|
1561
|
+
function renderWithCursor(value, cursorOffset, placeholder, showCursor) {
|
|
1562
|
+
if (!showCursor) {
|
|
1563
|
+
return value.length > 0 ? value : placeholder ? chalk.gray(placeholder) : "";
|
|
1564
|
+
}
|
|
1565
|
+
if (value.length === 0) {
|
|
1566
|
+
if (placeholder.length > 0) {
|
|
1567
|
+
return chalk.inverse(placeholder[0]) + chalk.gray(placeholder.slice(1));
|
|
1568
|
+
}
|
|
1569
|
+
return chalk.inverse(" ");
|
|
1570
|
+
}
|
|
1571
|
+
const chars = [...value];
|
|
1572
|
+
let rendered = "";
|
|
1573
|
+
for (let i = 0; i < chars.length; i++) {
|
|
1574
|
+
const char = chars[i] ?? "";
|
|
1575
|
+
rendered += i === cursorOffset ? chalk.inverse(char) : char;
|
|
1576
|
+
}
|
|
1577
|
+
if (cursorOffset >= chars.length) {
|
|
1578
|
+
rendered += chalk.inverse(" ");
|
|
1579
|
+
}
|
|
1580
|
+
return rendered;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/ui/WaveText.tsx
|
|
1584
|
+
import { useState as useState4, useEffect } from "react";
|
|
1585
|
+
import { Text as Text5 } from "ink";
|
|
1586
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1587
|
+
var WAVE_COLORS = ["#666666", "#888888", "#aaaaaa", "#888888"];
|
|
1588
|
+
var INTERVAL_MS = 400;
|
|
1589
|
+
var CHARS_PER_GROUP = 4;
|
|
1590
|
+
function WaveText({ text }) {
|
|
1591
|
+
const [tick, setTick] = useState4(0);
|
|
1592
|
+
useEffect(() => {
|
|
1593
|
+
const timer = setInterval(() => {
|
|
1594
|
+
setTick((prev) => prev + 1);
|
|
1595
|
+
}, INTERVAL_MS);
|
|
1596
|
+
return () => clearInterval(timer);
|
|
1597
|
+
}, []);
|
|
1598
|
+
const chars = [...text];
|
|
1599
|
+
return /* @__PURE__ */ jsx4(Text5, { children: chars.map((char, i) => {
|
|
1600
|
+
const group = Math.floor(i / CHARS_PER_GROUP);
|
|
1601
|
+
const colorIndex = (tick + group) % WAVE_COLORS.length;
|
|
1602
|
+
return /* @__PURE__ */ jsx4(Text5, { color: WAVE_COLORS[colorIndex], children: char }, i);
|
|
1603
|
+
}) });
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// src/ui/SlashAutocomplete.tsx
|
|
1607
|
+
import { Box as Box4, Text as Text6 } from "ink";
|
|
1608
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1609
|
+
var MAX_VISIBLE = 8;
|
|
1610
|
+
function CommandRow(props) {
|
|
1611
|
+
const { cmd, isSelected, showSlash } = props;
|
|
1612
|
+
const indicator = isSelected ? "\u25B8 " : " ";
|
|
1613
|
+
const nameColor = isSelected ? "cyan" : void 0;
|
|
1614
|
+
const dimmed = !isSelected;
|
|
1615
|
+
return /* @__PURE__ */ jsx5(Box4, { children: /* @__PURE__ */ jsxs4(Text6, { color: nameColor, dimColor: dimmed, children: [
|
|
1616
|
+
indicator,
|
|
1617
|
+
showSlash ? `/${cmd.name} ${cmd.description}` : cmd.description
|
|
1618
|
+
] }) });
|
|
1619
|
+
}
|
|
1620
|
+
function SlashAutocomplete({
|
|
1621
|
+
commands,
|
|
1622
|
+
selectedIndex,
|
|
1623
|
+
visible,
|
|
1624
|
+
isSubcommandMode
|
|
1625
|
+
}) {
|
|
1626
|
+
if (!visible || commands.length === 0) return null;
|
|
1627
|
+
const scrollOffset = computeScrollOffset(selectedIndex, commands.length);
|
|
1628
|
+
const visibleCommands = commands.slice(scrollOffset, scrollOffset + MAX_VISIBLE);
|
|
1629
|
+
return /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: visibleCommands.map((cmd, i) => /* @__PURE__ */ jsx5(
|
|
1630
|
+
CommandRow,
|
|
1631
|
+
{
|
|
1632
|
+
cmd,
|
|
1633
|
+
isSelected: scrollOffset + i === selectedIndex,
|
|
1634
|
+
showSlash: !isSubcommandMode
|
|
1635
|
+
},
|
|
1636
|
+
cmd.name
|
|
1637
|
+
)) });
|
|
1638
|
+
}
|
|
1639
|
+
function computeScrollOffset(selectedIndex, total) {
|
|
1640
|
+
if (total <= MAX_VISIBLE) return 0;
|
|
1641
|
+
if (selectedIndex < MAX_VISIBLE) return 0;
|
|
1642
|
+
const maxOffset = total - MAX_VISIBLE;
|
|
1643
|
+
return Math.min(selectedIndex - MAX_VISIBLE + 1, maxOffset);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/ui/InputArea.tsx
|
|
1647
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1648
|
+
function parseSlashInput(value) {
|
|
1649
|
+
if (!value.startsWith("/")) return { isSlash: false, parentCommand: "", filter: "" };
|
|
1650
|
+
const afterSlash = value.slice(1);
|
|
1651
|
+
const spaceIndex = afterSlash.indexOf(" ");
|
|
1652
|
+
if (spaceIndex === -1) return { isSlash: true, parentCommand: "", filter: afterSlash };
|
|
1653
|
+
const parent = afterSlash.slice(0, spaceIndex);
|
|
1654
|
+
const rest = afterSlash.slice(spaceIndex + 1);
|
|
1655
|
+
return { isSlash: true, parentCommand: parent, filter: rest };
|
|
1656
|
+
}
|
|
1657
|
+
function useAutocomplete(value, registry) {
|
|
1658
|
+
const [selectedIndex, setSelectedIndex] = useState5(0);
|
|
1659
|
+
const [dismissed, setDismissed] = useState5(false);
|
|
1660
|
+
const prevValueRef = React5.useRef(value);
|
|
1661
|
+
if (prevValueRef.current !== value) {
|
|
1662
|
+
prevValueRef.current = value;
|
|
1663
|
+
if (dismissed) setDismissed(false);
|
|
1664
|
+
}
|
|
1665
|
+
const parsed = parseSlashInput(value);
|
|
1666
|
+
const isSubcommandMode = parsed.isSlash && parsed.parentCommand.length > 0;
|
|
1667
|
+
const filteredCommands = useMemo2(() => {
|
|
1668
|
+
if (!registry || !parsed.isSlash || dismissed) return [];
|
|
1669
|
+
if (isSubcommandMode) {
|
|
1670
|
+
const subs = registry.getSubcommands(parsed.parentCommand);
|
|
1671
|
+
if (subs.length === 0) return [];
|
|
1672
|
+
if (!parsed.filter) return subs;
|
|
1673
|
+
const lower = parsed.filter.toLowerCase();
|
|
1674
|
+
return subs.filter((c) => c.name.toLowerCase().startsWith(lower));
|
|
1675
|
+
}
|
|
1676
|
+
return registry.getCommands(parsed.filter);
|
|
1677
|
+
}, [registry, parsed.isSlash, parsed.parentCommand, parsed.filter, dismissed, isSubcommandMode]);
|
|
1678
|
+
const showPopup = parsed.isSlash && filteredCommands.length > 0 && !dismissed;
|
|
1679
|
+
if (selectedIndex >= filteredCommands.length && filteredCommands.length > 0) {
|
|
1680
|
+
setSelectedIndex(filteredCommands.length - 1);
|
|
1681
|
+
}
|
|
1682
|
+
return {
|
|
1683
|
+
showPopup,
|
|
1684
|
+
filteredCommands,
|
|
1685
|
+
selectedIndex,
|
|
1686
|
+
setSelectedIndex,
|
|
1687
|
+
isSubcommandMode,
|
|
1688
|
+
setShowPopup: (val) => {
|
|
1689
|
+
if (typeof val === "function") {
|
|
1690
|
+
setDismissed((prev) => {
|
|
1691
|
+
const nextVal = val(!prev);
|
|
1692
|
+
return !nextVal;
|
|
1693
|
+
});
|
|
1694
|
+
} else {
|
|
1695
|
+
setDismissed(!val);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
function InputArea({ onSubmit, isDisabled, registry }) {
|
|
1701
|
+
const [value, setValue] = useState5("");
|
|
1702
|
+
const {
|
|
1703
|
+
showPopup,
|
|
1704
|
+
filteredCommands,
|
|
1705
|
+
selectedIndex,
|
|
1706
|
+
setSelectedIndex,
|
|
1707
|
+
isSubcommandMode,
|
|
1708
|
+
setShowPopup
|
|
1709
|
+
} = useAutocomplete(value, registry);
|
|
1710
|
+
const handleSubmit = useCallback5(
|
|
1711
|
+
(text) => {
|
|
1712
|
+
const trimmed = text.trim();
|
|
1713
|
+
if (trimmed.length === 0) return;
|
|
1714
|
+
if (showPopup && filteredCommands[selectedIndex]) {
|
|
1715
|
+
selectCommand(filteredCommands[selectedIndex]);
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
setValue("");
|
|
1719
|
+
onSubmit(trimmed);
|
|
1720
|
+
},
|
|
1721
|
+
[showPopup, filteredCommands, selectedIndex, onSubmit]
|
|
1722
|
+
);
|
|
1723
|
+
const selectCommand = useCallback5(
|
|
1724
|
+
(cmd) => {
|
|
1725
|
+
const parsed = parseSlashInput(value);
|
|
1726
|
+
if (parsed.parentCommand) {
|
|
1727
|
+
const fullCommand = `/${parsed.parentCommand} ${cmd.name}`;
|
|
1728
|
+
setValue("");
|
|
1729
|
+
onSubmit(fullCommand);
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
if (cmd.subcommands && cmd.subcommands.length > 0) {
|
|
1733
|
+
setValue(`/${cmd.name} `);
|
|
1734
|
+
setSelectedIndex(0);
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
setValue("");
|
|
1738
|
+
onSubmit(`/${cmd.name}`);
|
|
1739
|
+
},
|
|
1740
|
+
[value, onSubmit, setSelectedIndex]
|
|
1741
|
+
);
|
|
1742
|
+
useInput2(
|
|
1743
|
+
(_input, key) => {
|
|
1744
|
+
if (!showPopup) return;
|
|
1745
|
+
if (key.upArrow) {
|
|
1746
|
+
setSelectedIndex((prev) => prev > 0 ? prev - 1 : filteredCommands.length - 1);
|
|
1747
|
+
} else if (key.downArrow) {
|
|
1748
|
+
setSelectedIndex((prev) => prev < filteredCommands.length - 1 ? prev + 1 : 0);
|
|
1749
|
+
} else if (key.escape) {
|
|
1750
|
+
setShowPopup(false);
|
|
1751
|
+
} else if (key.tab) {
|
|
1752
|
+
const cmd = filteredCommands[selectedIndex];
|
|
1753
|
+
if (cmd) selectCommand(cmd);
|
|
1754
|
+
}
|
|
1755
|
+
},
|
|
1756
|
+
{ isActive: showPopup && !isDisabled }
|
|
1757
|
+
);
|
|
1758
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
1759
|
+
showPopup && /* @__PURE__ */ jsx6(
|
|
1760
|
+
SlashAutocomplete,
|
|
1761
|
+
{
|
|
1762
|
+
commands: filteredCommands,
|
|
1763
|
+
selectedIndex,
|
|
1764
|
+
visible: showPopup,
|
|
1765
|
+
isSubcommandMode
|
|
1766
|
+
}
|
|
1767
|
+
),
|
|
1768
|
+
/* @__PURE__ */ jsx6(Box5, { borderStyle: "single", borderColor: isDisabled ? "gray" : "green", paddingLeft: 1, children: isDisabled ? /* @__PURE__ */ jsx6(WaveText, { text: " Waiting for response..." }) : /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1769
|
+
/* @__PURE__ */ jsx6(Text7, { color: "green", bold: true, children: "> " }),
|
|
1770
|
+
/* @__PURE__ */ jsx6(
|
|
1771
|
+
CjkTextInput,
|
|
1772
|
+
{
|
|
1773
|
+
value,
|
|
1774
|
+
onChange: setValue,
|
|
1775
|
+
onSubmit: handleSubmit,
|
|
1776
|
+
placeholder: "Type a message or /help"
|
|
1777
|
+
}
|
|
1778
|
+
)
|
|
1779
|
+
] }) })
|
|
1780
|
+
] });
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// src/ui/ConfirmPrompt.tsx
|
|
1784
|
+
import { useState as useState6, useCallback as useCallback6, useRef as useRef4 } from "react";
|
|
1785
|
+
import { Box as Box6, Text as Text8, useInput as useInput3 } from "ink";
|
|
1786
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1787
|
+
function ConfirmPrompt({
|
|
1788
|
+
message,
|
|
1789
|
+
options = ["Yes", "No"],
|
|
1790
|
+
onSelect
|
|
1791
|
+
}) {
|
|
1792
|
+
const [selected, setSelected] = useState6(0);
|
|
1793
|
+
const resolvedRef = useRef4(false);
|
|
1794
|
+
const doSelect = useCallback6(
|
|
1795
|
+
(index) => {
|
|
1796
|
+
if (resolvedRef.current) return;
|
|
1797
|
+
resolvedRef.current = true;
|
|
1798
|
+
onSelect(index);
|
|
1799
|
+
},
|
|
1800
|
+
[onSelect]
|
|
1801
|
+
);
|
|
1802
|
+
useInput3((input, key) => {
|
|
1803
|
+
if (resolvedRef.current) return;
|
|
1804
|
+
if (key.leftArrow || key.upArrow) {
|
|
1805
|
+
setSelected((prev) => prev > 0 ? prev - 1 : prev);
|
|
1806
|
+
} else if (key.rightArrow || key.downArrow) {
|
|
1807
|
+
setSelected((prev) => prev < options.length - 1 ? prev + 1 : prev);
|
|
1808
|
+
} else if (key.return) {
|
|
1809
|
+
doSelect(selected);
|
|
1810
|
+
} else if (input === "y" && options.length === 2) {
|
|
1811
|
+
doSelect(0);
|
|
1812
|
+
} else if (input === "n" && options.length === 2) {
|
|
1813
|
+
doSelect(1);
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
1817
|
+
/* @__PURE__ */ jsx7(Text8, { color: "yellow", children: message }),
|
|
1818
|
+
/* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsx7(Box6, { marginRight: 2, children: /* @__PURE__ */ jsxs6(Text8, { color: i === selected ? "cyan" : void 0, bold: i === selected, children: [
|
|
1819
|
+
i === selected ? "> " : " ",
|
|
1820
|
+
opt
|
|
1821
|
+
] }) }, opt)) }),
|
|
1822
|
+
/* @__PURE__ */ jsx7(Text8, { dimColor: true, children: " arrow keys to select, Enter to confirm" })
|
|
1823
|
+
] });
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// src/ui/PermissionPrompt.tsx
|
|
1827
|
+
import React7 from "react";
|
|
1828
|
+
import { Box as Box7, Text as Text9, useInput as useInput4 } from "ink";
|
|
1829
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1830
|
+
var OPTIONS = ["Allow", "Allow always (this session)", "Deny"];
|
|
1831
|
+
function formatArgs(args) {
|
|
1832
|
+
const entries = Object.entries(args);
|
|
1833
|
+
if (entries.length === 0) return "(no arguments)";
|
|
1834
|
+
return entries.map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`).join(", ");
|
|
1835
|
+
}
|
|
1836
|
+
function PermissionPrompt({ request }) {
|
|
1837
|
+
const [selected, setSelected] = React7.useState(0);
|
|
1838
|
+
const resolvedRef = React7.useRef(false);
|
|
1839
|
+
const prevRequestRef = React7.useRef(request);
|
|
1840
|
+
if (prevRequestRef.current !== request) {
|
|
1841
|
+
prevRequestRef.current = request;
|
|
1842
|
+
resolvedRef.current = false;
|
|
1843
|
+
setSelected(0);
|
|
1844
|
+
}
|
|
1845
|
+
const doResolve = React7.useCallback(
|
|
1846
|
+
(index) => {
|
|
1847
|
+
if (resolvedRef.current) return;
|
|
1848
|
+
resolvedRef.current = true;
|
|
1849
|
+
if (index === 0) request.resolve(true);
|
|
1850
|
+
else if (index === 1) request.resolve("allow-session");
|
|
1851
|
+
else request.resolve(false);
|
|
1852
|
+
},
|
|
1853
|
+
[request]
|
|
1854
|
+
);
|
|
1855
|
+
useInput4((input, key) => {
|
|
1856
|
+
if (resolvedRef.current) return;
|
|
1857
|
+
if (key.upArrow || key.leftArrow) {
|
|
1858
|
+
setSelected((prev) => prev > 0 ? prev - 1 : prev);
|
|
1859
|
+
} else if (key.downArrow || key.rightArrow) {
|
|
1860
|
+
setSelected((prev) => prev < OPTIONS.length - 1 ? prev + 1 : prev);
|
|
1861
|
+
} else if (key.return) {
|
|
1862
|
+
doResolve(selected);
|
|
1863
|
+
} else if (input === "y" || input === "1") {
|
|
1864
|
+
doResolve(0);
|
|
1865
|
+
} else if (input === "a" || input === "2") {
|
|
1866
|
+
doResolve(1);
|
|
1867
|
+
} else if (input === "n" || input === "d" || input === "3") {
|
|
1868
|
+
doResolve(2);
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
1872
|
+
/* @__PURE__ */ jsx8(Text9, { color: "yellow", bold: true, children: "[Permission Required]" }),
|
|
1873
|
+
/* @__PURE__ */ jsxs7(Text9, { children: [
|
|
1874
|
+
"Tool:",
|
|
1875
|
+
" ",
|
|
1876
|
+
/* @__PURE__ */ jsx8(Text9, { color: "cyan", bold: true, children: request.toolName })
|
|
1877
|
+
] }),
|
|
1878
|
+
/* @__PURE__ */ jsxs7(Text9, { dimColor: true, children: [
|
|
1879
|
+
" ",
|
|
1880
|
+
formatArgs(request.toolArgs)
|
|
1881
|
+
] }),
|
|
1882
|
+
/* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: OPTIONS.map((opt, i) => /* @__PURE__ */ jsx8(Box7, { marginRight: 2, children: /* @__PURE__ */ jsxs7(Text9, { color: i === selected ? "cyan" : void 0, bold: i === selected, children: [
|
|
1883
|
+
i === selected ? "> " : " ",
|
|
1884
|
+
opt
|
|
1885
|
+
] }) }, opt)) }),
|
|
1886
|
+
/* @__PURE__ */ jsx8(Text9, { dimColor: true, children: " left/right to select, Enter to confirm" })
|
|
1887
|
+
] });
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// src/ui/StreamingIndicator.tsx
|
|
1891
|
+
import { Box as Box8, Text as Text10 } from "ink";
|
|
1892
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1893
|
+
function getToolStyle(t) {
|
|
1894
|
+
if (t.isRunning) return { color: "yellow", icon: "\u27F3", strikethrough: false };
|
|
1895
|
+
if (t.result === "error") return { color: "red", icon: "\u2717", strikethrough: true };
|
|
1896
|
+
if (t.result === "denied") return { color: "yellowBright", icon: "\u2298", strikethrough: true };
|
|
1897
|
+
return { color: "green", icon: "\u2713", strikethrough: false };
|
|
1898
|
+
}
|
|
1899
|
+
function StreamingIndicator({ text, activeTools }) {
|
|
1900
|
+
const hasTools = activeTools.length > 0;
|
|
1901
|
+
const hasText = text.length > 0;
|
|
1902
|
+
if (!hasTools && !hasText) {
|
|
1903
|
+
return /* @__PURE__ */ jsx9(Text10, { color: "yellow", children: "Thinking..." });
|
|
1904
|
+
}
|
|
1905
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1906
|
+
hasTools && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, children: [
|
|
1907
|
+
/* @__PURE__ */ jsx9(Text10, { color: "white", bold: true, children: "Tools:" }),
|
|
1908
|
+
/* @__PURE__ */ jsx9(Text10, { children: " " }),
|
|
1909
|
+
activeTools.map((t, i) => {
|
|
1910
|
+
const { color, icon, strikethrough } = getToolStyle(t);
|
|
1911
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1912
|
+
/* @__PURE__ */ jsxs8(Text10, { color, strikethrough, children: [
|
|
1913
|
+
" ",
|
|
1914
|
+
icon,
|
|
1915
|
+
" ",
|
|
1916
|
+
t.toolName,
|
|
1917
|
+
"(",
|
|
1918
|
+
t.firstArg,
|
|
1919
|
+
")"
|
|
1920
|
+
] }),
|
|
1921
|
+
t.diffLines && t.diffLines.length > 0 && /* @__PURE__ */ jsx9(DiffBlock, { file: t.diffFile, lines: t.diffLines })
|
|
1922
|
+
] }, `${t.toolName}-${i}`);
|
|
1923
|
+
})
|
|
1924
|
+
] }),
|
|
1925
|
+
hasText && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, children: [
|
|
1926
|
+
/* @__PURE__ */ jsx9(Text10, { color: "cyan", bold: true, children: "Robota:" }),
|
|
1927
|
+
/* @__PURE__ */ jsx9(Text10, { children: " " }),
|
|
1928
|
+
/* @__PURE__ */ jsx9(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text10, { wrap: "wrap", children: renderMarkdown(text) }) })
|
|
1929
|
+
] })
|
|
1930
|
+
] });
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// src/ui/App.tsx
|
|
1934
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1935
|
+
var EXIT_DELAY_MS2 = 500;
|
|
1936
|
+
function mergeHooksIntoConfig(configHooks, pluginHooks) {
|
|
1937
|
+
const pluginKeys = Object.keys(pluginHooks);
|
|
1938
|
+
if (pluginKeys.length === 0) return configHooks;
|
|
1939
|
+
const merged = {};
|
|
1940
|
+
for (const [event, groups] of Object.entries(pluginHooks)) {
|
|
1941
|
+
merged[event] = [...groups];
|
|
1942
|
+
}
|
|
1943
|
+
if (configHooks) {
|
|
1944
|
+
for (const [event, groups] of Object.entries(configHooks)) {
|
|
1945
|
+
if (!Array.isArray(groups)) continue;
|
|
1946
|
+
if (!merged[event]) merged[event] = [];
|
|
1947
|
+
merged[event].push(...groups);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
return merged;
|
|
1951
|
+
}
|
|
1952
|
+
function App(props) {
|
|
1953
|
+
const { exit } = useApp();
|
|
1954
|
+
const { registry, pluginHooks } = useCommandRegistry(props.cwd ?? process.cwd());
|
|
1955
|
+
const configWithPluginHooks = {
|
|
1956
|
+
...props.config,
|
|
1957
|
+
hooks: mergeHooksIntoConfig(
|
|
1958
|
+
props.config.hooks,
|
|
1959
|
+
pluginHooks
|
|
1960
|
+
)
|
|
1961
|
+
};
|
|
1962
|
+
const { session, permissionRequest, streamingText, clearStreamingText, activeTools } = useSession(
|
|
1963
|
+
{ ...props, config: configWithPluginHooks }
|
|
1964
|
+
);
|
|
1965
|
+
const { messages, setMessages, addMessage } = useMessages();
|
|
1966
|
+
const [isThinking, setIsThinking] = useState7(false);
|
|
1967
|
+
const initialCtx = session.getContextState();
|
|
1968
|
+
const [contextState, setContextState] = useState7({
|
|
1969
|
+
percentage: initialCtx.usedPercentage,
|
|
1970
|
+
usedTokens: initialCtx.usedTokens,
|
|
1971
|
+
maxTokens: initialCtx.maxTokens
|
|
1972
|
+
});
|
|
1973
|
+
const pendingModelChangeRef = useRef5(null);
|
|
1974
|
+
const [pendingModelId, setPendingModelId] = useState7(null);
|
|
1975
|
+
const pluginCallbacks = usePluginCallbacks(props.cwd ?? process.cwd());
|
|
1976
|
+
const handleSlashCommand = useSlashCommands(
|
|
1977
|
+
session,
|
|
1978
|
+
addMessage,
|
|
1979
|
+
setMessages,
|
|
1980
|
+
exit,
|
|
1981
|
+
registry,
|
|
1982
|
+
pendingModelChangeRef,
|
|
1983
|
+
setPendingModelId,
|
|
1984
|
+
pluginCallbacks
|
|
1985
|
+
);
|
|
1986
|
+
const handleSubmit = useSubmitHandler(
|
|
1987
|
+
session,
|
|
1988
|
+
addMessage,
|
|
1989
|
+
handleSlashCommand,
|
|
1990
|
+
clearStreamingText,
|
|
1991
|
+
setIsThinking,
|
|
1992
|
+
setContextState,
|
|
1993
|
+
registry
|
|
1994
|
+
);
|
|
1995
|
+
useInput5(
|
|
1996
|
+
(_input, key) => {
|
|
1997
|
+
if (key.ctrl && _input === "c") exit();
|
|
1998
|
+
if (key.escape && isThinking) session.abort();
|
|
1999
|
+
},
|
|
2000
|
+
{ isActive: !permissionRequest }
|
|
2001
|
+
);
|
|
2002
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
2003
|
+
/* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
|
|
2004
|
+
/* @__PURE__ */ jsx10(Text11, { color: "cyan", bold: true, children: `
|
|
2005
|
+
____ ___ ____ ___ _____ _
|
|
2006
|
+
| _ \\ / _ \\| __ ) / _ \\_ _|/ \\
|
|
2007
|
+
| |_) | | | | _ \\| | | || | / _ \\
|
|
2008
|
+
| _ <| |_| | |_) | |_| || |/ ___ \\
|
|
2009
|
+
|_| \\_\\\\___/|____/ \\___/ |_/_/ \\_\\
|
|
2010
|
+
` }),
|
|
2011
|
+
/* @__PURE__ */ jsxs9(Text11, { dimColor: true, children: [
|
|
2012
|
+
" v",
|
|
2013
|
+
props.version ?? "0.0.0"
|
|
2014
|
+
] })
|
|
2015
|
+
] }),
|
|
2016
|
+
/* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
|
|
2017
|
+
/* @__PURE__ */ jsx10(MessageList, { messages }),
|
|
2018
|
+
isThinking && /* @__PURE__ */ jsx10(Box9, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx10(StreamingIndicator, { text: streamingText, activeTools }) })
|
|
2019
|
+
] }),
|
|
2020
|
+
permissionRequest && /* @__PURE__ */ jsx10(PermissionPrompt, { request: permissionRequest }),
|
|
2021
|
+
pendingModelId && /* @__PURE__ */ jsx10(
|
|
2022
|
+
ConfirmPrompt,
|
|
2023
|
+
{
|
|
2024
|
+
message: `Change model to ${getModelName(pendingModelId)}? This will restart the session.`,
|
|
2025
|
+
onSelect: (index) => {
|
|
2026
|
+
setPendingModelId(null);
|
|
2027
|
+
pendingModelChangeRef.current = null;
|
|
2028
|
+
if (index === 0) {
|
|
2029
|
+
try {
|
|
2030
|
+
const settingsPath = getUserSettingsPath();
|
|
2031
|
+
updateModelInSettings(settingsPath, pendingModelId);
|
|
2032
|
+
addMessage({
|
|
2033
|
+
role: "system",
|
|
2034
|
+
content: `Model changed to ${getModelName(pendingModelId)}. Restarting...`
|
|
2035
|
+
});
|
|
2036
|
+
setTimeout(() => exit(), EXIT_DELAY_MS2);
|
|
2037
|
+
} catch (err) {
|
|
2038
|
+
addMessage({
|
|
2039
|
+
role: "system",
|
|
2040
|
+
content: `Failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
} else {
|
|
2044
|
+
addMessage({ role: "system", content: "Model change cancelled." });
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
),
|
|
2049
|
+
/* @__PURE__ */ jsx10(
|
|
2050
|
+
StatusBar,
|
|
2051
|
+
{
|
|
2052
|
+
permissionMode: session.getPermissionMode(),
|
|
2053
|
+
modelName: getModelName(props.config.provider.model),
|
|
2054
|
+
sessionId: session.getSessionId(),
|
|
2055
|
+
messageCount: messages.length,
|
|
2056
|
+
isThinking,
|
|
2057
|
+
contextPercentage: contextState.percentage,
|
|
2058
|
+
contextUsedTokens: contextState.usedTokens,
|
|
2059
|
+
contextMaxTokens: contextState.maxTokens
|
|
2060
|
+
}
|
|
2061
|
+
),
|
|
2062
|
+
/* @__PURE__ */ jsx10(
|
|
2063
|
+
InputArea,
|
|
2064
|
+
{
|
|
2065
|
+
onSubmit: handleSubmit,
|
|
2066
|
+
isDisabled: isThinking || !!permissionRequest,
|
|
2067
|
+
registry
|
|
2068
|
+
}
|
|
2069
|
+
),
|
|
2070
|
+
/* @__PURE__ */ jsx10(Text11, { children: " " })
|
|
2071
|
+
] });
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/ui/render.tsx
|
|
2075
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
2076
|
+
function renderApp(options) {
|
|
2077
|
+
process.on("unhandledRejection", (reason) => {
|
|
2078
|
+
process.stderr.write(`
|
|
2079
|
+
[UNHANDLED REJECTION] ${reason}
|
|
2080
|
+
`);
|
|
2081
|
+
if (reason instanceof Error) {
|
|
2082
|
+
process.stderr.write(`${reason.stack}
|
|
2083
|
+
`);
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
const instance = render(/* @__PURE__ */ jsx11(App, { ...options }), {
|
|
2087
|
+
exitOnCtrlC: true
|
|
2088
|
+
});
|
|
2089
|
+
instance.waitUntilExit().catch((err) => {
|
|
2090
|
+
if (err) {
|
|
2091
|
+
process.stderr.write(`
|
|
2092
|
+
[EXIT ERROR] ${err}
|
|
2093
|
+
`);
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// src/cli.ts
|
|
2099
|
+
function checkSettingsFile(filePath) {
|
|
2100
|
+
if (!existsSync3(filePath)) return "missing";
|
|
2101
|
+
try {
|
|
2102
|
+
const raw = readFileSync3(filePath, "utf8").trim();
|
|
2103
|
+
if (raw.length === 0) return "incomplete";
|
|
2104
|
+
const parsed = JSON.parse(raw);
|
|
2105
|
+
const provider = parsed.provider;
|
|
2106
|
+
if (!provider?.apiKey) return "incomplete";
|
|
2107
|
+
return "valid";
|
|
2108
|
+
} catch {
|
|
2109
|
+
return "corrupt";
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
function readVersion() {
|
|
2113
|
+
try {
|
|
2114
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
2115
|
+
const dir = dirname3(thisFile);
|
|
2116
|
+
const candidates = [join5(dir, "..", "..", "package.json"), join5(dir, "..", "package.json")];
|
|
2117
|
+
for (const pkgPath of candidates) {
|
|
2118
|
+
try {
|
|
2119
|
+
const raw = readFileSync3(pkgPath, "utf-8");
|
|
2120
|
+
const pkg = JSON.parse(raw);
|
|
2121
|
+
if (pkg.version !== void 0 && pkg.name !== void 0) {
|
|
2122
|
+
return pkg.version;
|
|
2123
|
+
}
|
|
2124
|
+
} catch {
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
return "0.0.0";
|
|
2128
|
+
} catch {
|
|
2129
|
+
return "0.0.0";
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
function promptInput(label, masked = false) {
|
|
2133
|
+
return new Promise((resolve) => {
|
|
2134
|
+
process.stdout.write(label);
|
|
2135
|
+
let input = "";
|
|
2136
|
+
const stdin = process.stdin;
|
|
2137
|
+
const wasRaw = stdin.isRaw;
|
|
2138
|
+
stdin.setRawMode(true);
|
|
2139
|
+
stdin.resume();
|
|
2140
|
+
stdin.setEncoding("utf8");
|
|
2141
|
+
const onData = (data) => {
|
|
2142
|
+
for (const ch of data) {
|
|
2143
|
+
if (ch === "\r" || ch === "\n") {
|
|
2144
|
+
stdin.removeListener("data", onData);
|
|
2145
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
2146
|
+
stdin.pause();
|
|
2147
|
+
process.stdout.write("\n");
|
|
2148
|
+
resolve(input.trim());
|
|
2149
|
+
return;
|
|
2150
|
+
} else if (ch === "\x7F" || ch === "\b") {
|
|
2151
|
+
if (input.length > 0) {
|
|
2152
|
+
input = input.slice(0, -1);
|
|
2153
|
+
process.stdout.write("\b \b");
|
|
2154
|
+
}
|
|
2155
|
+
} else if (ch === "") {
|
|
2156
|
+
process.stdout.write("\n");
|
|
2157
|
+
process.exit(0);
|
|
2158
|
+
} else if (ch.charCodeAt(0) >= 32) {
|
|
2159
|
+
input += ch;
|
|
2160
|
+
process.stdout.write(masked ? "*" : ch);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
stdin.on("data", onData);
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
async function ensureConfig(cwd) {
|
|
2168
|
+
const userPath = getUserSettingsPath();
|
|
2169
|
+
const projectPath = join5(cwd, ".robota", "settings.json");
|
|
2170
|
+
const localPath = join5(cwd, ".robota", "settings.local.json");
|
|
2171
|
+
const paths = [userPath, projectPath, localPath];
|
|
2172
|
+
const checks = paths.map((p) => ({ path: p, status: checkSettingsFile(p) }));
|
|
2173
|
+
if (checks.some((c) => c.status === "valid")) {
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
const corrupt = checks.filter((c) => c.status === "corrupt");
|
|
2177
|
+
const incomplete = checks.filter((c) => c.status === "incomplete");
|
|
2178
|
+
process.stdout.write("\n");
|
|
2179
|
+
if (corrupt.length > 0) {
|
|
2180
|
+
for (const c of corrupt) {
|
|
2181
|
+
process.stderr.write(` ERROR: Settings file is corrupt (invalid JSON): ${c.path}
|
|
2182
|
+
`);
|
|
2183
|
+
}
|
|
2184
|
+
process.stdout.write("\n");
|
|
2185
|
+
}
|
|
2186
|
+
if (incomplete.length > 0) {
|
|
2187
|
+
for (const c of incomplete) {
|
|
2188
|
+
process.stderr.write(` WARNING: Settings file is missing provider.apiKey: ${c.path}
|
|
2189
|
+
`);
|
|
2190
|
+
}
|
|
2191
|
+
process.stdout.write("\n");
|
|
2192
|
+
}
|
|
2193
|
+
if (corrupt.length === 0 && incomplete.length === 0) {
|
|
2194
|
+
process.stdout.write(" Welcome to Robota CLI!\n");
|
|
2195
|
+
process.stdout.write(" No configuration found. Let's set up.\n");
|
|
2196
|
+
} else {
|
|
2197
|
+
process.stdout.write(" Reconfiguring...\n");
|
|
2198
|
+
}
|
|
2199
|
+
process.stdout.write("\n");
|
|
2200
|
+
const apiKey = await promptInput(" Anthropic API key: ", true);
|
|
2201
|
+
if (!apiKey) {
|
|
2202
|
+
process.stderr.write("\n No API key provided. Exiting.\n");
|
|
2203
|
+
process.exit(1);
|
|
2204
|
+
}
|
|
2205
|
+
const language = await promptInput(" Response language (ko/en/ja/zh, default: en): ");
|
|
2206
|
+
const settingsDir = dirname3(userPath);
|
|
2207
|
+
mkdirSync2(settingsDir, { recursive: true });
|
|
2208
|
+
const settings = {
|
|
2209
|
+
provider: {
|
|
2210
|
+
name: "anthropic",
|
|
2211
|
+
model: "claude-sonnet-4-6",
|
|
2212
|
+
apiKey
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
if (language) {
|
|
2216
|
+
settings.language = language;
|
|
2217
|
+
}
|
|
2218
|
+
writeFileSync2(userPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
2219
|
+
process.stdout.write(`
|
|
2220
|
+
Config saved to ${userPath}
|
|
2221
|
+
|
|
2222
|
+
`);
|
|
2223
|
+
}
|
|
2224
|
+
function resetConfig() {
|
|
2225
|
+
const userPath = getUserSettingsPath();
|
|
2226
|
+
if (deleteSettings(userPath)) {
|
|
2227
|
+
process.stdout.write(`Deleted ${userPath}
|
|
2228
|
+
`);
|
|
2229
|
+
} else {
|
|
2230
|
+
process.stdout.write("No user settings found.\n");
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
async function startCli() {
|
|
2234
|
+
const args = parseCliArgs();
|
|
2235
|
+
if (args.version) {
|
|
2236
|
+
process.stdout.write(`robota ${readVersion()}
|
|
2237
|
+
`);
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
if (args.reset) {
|
|
2241
|
+
resetConfig();
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
const cwd = process.cwd();
|
|
2245
|
+
await ensureConfig(cwd);
|
|
2246
|
+
const [config, context, projectInfo] = await Promise.all([
|
|
2247
|
+
loadConfig(cwd),
|
|
2248
|
+
loadContext(cwd),
|
|
2249
|
+
detectProject(cwd)
|
|
2250
|
+
]);
|
|
2251
|
+
if (args.model !== void 0) {
|
|
2252
|
+
config.provider.model = args.model;
|
|
2253
|
+
}
|
|
2254
|
+
if (args.language !== void 0) {
|
|
2255
|
+
config.language = args.language;
|
|
2256
|
+
}
|
|
2257
|
+
const sessionStore = new SessionStore();
|
|
2258
|
+
if (args.printMode) {
|
|
2259
|
+
const prompt = args.positional.join(" ").trim();
|
|
2260
|
+
if (prompt.length === 0) {
|
|
2261
|
+
process.stderr.write("Print mode (-p) requires a prompt argument.\n");
|
|
2262
|
+
process.exit(1);
|
|
2263
|
+
}
|
|
2264
|
+
const terminal = new PrintTerminal();
|
|
2265
|
+
const paths = projectPaths2(cwd);
|
|
2266
|
+
const session = createSession2({
|
|
2267
|
+
config,
|
|
2268
|
+
context,
|
|
2269
|
+
terminal,
|
|
2270
|
+
sessionLogger: new FileSessionLogger2(paths.logs),
|
|
2271
|
+
projectInfo,
|
|
2272
|
+
permissionMode: args.permissionMode,
|
|
2273
|
+
promptForApproval
|
|
2274
|
+
});
|
|
2275
|
+
const response = await session.run(prompt);
|
|
2276
|
+
process.stdout.write(response + "\n");
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
renderApp({
|
|
2280
|
+
config,
|
|
2281
|
+
context,
|
|
2282
|
+
projectInfo,
|
|
2283
|
+
sessionStore,
|
|
2284
|
+
permissionMode: args.permissionMode,
|
|
2285
|
+
maxTurns: args.maxTurns,
|
|
2286
|
+
version: readVersion()
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
export {
|
|
2291
|
+
startCli
|
|
2292
|
+
};
|