@robota-sdk/agent-cli 3.0.0-beta.16 → 3.0.0-beta.18

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.
@@ -40,22 +40,273 @@ module.exports = __toCommonJS(index_exports);
40
40
  var import_agent_sdk4 = require("@robota-sdk/agent-sdk");
41
41
 
42
42
  // src/cli.ts
43
- var import_node_util = require("util");
44
43
  var import_node_fs3 = require("fs");
45
44
  var import_node_path3 = require("path");
46
45
  var import_node_url = require("url");
47
- var readline = __toESM(require("readline"), 1);
48
46
  var import_agent_sdk2 = require("@robota-sdk/agent-sdk");
49
47
  var import_agent_sdk3 = require("@robota-sdk/agent-sdk");
50
48
 
49
+ // src/utils/cli-args.ts
50
+ var import_node_util = require("util");
51
+ var VALID_MODES = ["plan", "default", "acceptEdits", "bypassPermissions"];
52
+ function parsePermissionMode(raw) {
53
+ if (raw === void 0) return void 0;
54
+ if (!VALID_MODES.includes(raw)) {
55
+ process.stderr.write(`Invalid --permission-mode "${raw}". Valid: ${VALID_MODES.join(" | ")}
56
+ `);
57
+ process.exit(1);
58
+ }
59
+ return raw;
60
+ }
61
+ function parseMaxTurns(raw) {
62
+ if (raw === void 0) return void 0;
63
+ const n = parseInt(raw, 10);
64
+ if (isNaN(n) || n <= 0) {
65
+ process.stderr.write(`Invalid --max-turns "${raw}". Must be a positive integer.
66
+ `);
67
+ process.exit(1);
68
+ }
69
+ return n;
70
+ }
71
+ function parseCliArgs() {
72
+ const { values, positionals } = (0, import_node_util.parseArgs)({
73
+ allowPositionals: true,
74
+ options: {
75
+ p: { type: "boolean", short: "p", default: false },
76
+ c: { type: "boolean", short: "c", default: false },
77
+ r: { type: "string", short: "r" },
78
+ model: { type: "string" },
79
+ "permission-mode": { type: "string" },
80
+ "max-turns": { type: "string" },
81
+ version: { type: "boolean", default: false },
82
+ reset: { type: "boolean", default: false }
83
+ }
84
+ });
85
+ return {
86
+ positional: positionals,
87
+ printMode: values["p"] ?? false,
88
+ continueMode: values["c"] ?? false,
89
+ resumeId: values["r"],
90
+ model: values["model"],
91
+ permissionMode: parsePermissionMode(values["permission-mode"]),
92
+ maxTurns: parseMaxTurns(values["max-turns"]),
93
+ version: values["version"] ?? false,
94
+ reset: values["reset"] ?? false
95
+ };
96
+ }
97
+
98
+ // src/utils/settings-io.ts
99
+ var import_node_fs = require("fs");
100
+ var import_node_path = require("path");
101
+ function getUserSettingsPath() {
102
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
103
+ return (0, import_node_path.join)(home, ".robota", "settings.json");
104
+ }
105
+ function readSettings(path) {
106
+ if (!(0, import_node_fs.existsSync)(path)) return {};
107
+ return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
108
+ }
109
+ function writeSettings(path, settings) {
110
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
111
+ (0, import_node_fs.writeFileSync)(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
112
+ }
113
+ function updateModelInSettings(settingsPath, modelId) {
114
+ const settings = readSettings(settingsPath);
115
+ const provider = settings.provider ?? {};
116
+ provider.model = modelId;
117
+ settings.provider = provider;
118
+ writeSettings(settingsPath, settings);
119
+ }
120
+ function deleteSettings(path) {
121
+ if ((0, import_node_fs.existsSync)(path)) {
122
+ (0, import_node_fs.unlinkSync)(path);
123
+ return true;
124
+ }
125
+ return false;
126
+ }
127
+
128
+ // src/print-terminal.ts
129
+ var readline = __toESM(require("readline"), 1);
130
+ var PrintTerminal = class {
131
+ write(text) {
132
+ process.stdout.write(text);
133
+ }
134
+ writeLine(text) {
135
+ process.stdout.write(text + "\n");
136
+ }
137
+ writeMarkdown(md) {
138
+ process.stdout.write(md);
139
+ }
140
+ writeError(text) {
141
+ process.stderr.write(text + "\n");
142
+ }
143
+ prompt(question) {
144
+ return new Promise((resolve) => {
145
+ const rl = readline.createInterface({
146
+ input: process.stdin,
147
+ output: process.stdout,
148
+ terminal: false,
149
+ historySize: 0
150
+ });
151
+ rl.question(question, (answer) => {
152
+ rl.close();
153
+ resolve(answer);
154
+ });
155
+ });
156
+ }
157
+ async select(options, initialIndex = 0) {
158
+ for (let i = 0; i < options.length; i++) {
159
+ const marker = i === initialIndex ? ">" : " ";
160
+ process.stdout.write(` ${marker} ${i + 1}) ${options[i]}
161
+ `);
162
+ }
163
+ const answer = await this.prompt(
164
+ ` Choose [1-${options.length}] (default: ${options[initialIndex]}): `
165
+ );
166
+ const trimmed = answer.trim().toLowerCase();
167
+ if (trimmed === "") return initialIndex;
168
+ const num = parseInt(trimmed, 10);
169
+ if (!isNaN(num) && num >= 1 && num <= options.length) return num - 1;
170
+ return initialIndex;
171
+ }
172
+ spinner(_message) {
173
+ return { stop() {
174
+ }, update() {
175
+ } };
176
+ }
177
+ };
178
+
51
179
  // src/ui/render.tsx
52
- var import_ink10 = require("ink");
180
+ var import_ink11 = require("ink");
53
181
 
54
182
  // src/ui/App.tsx
55
183
  var import_react6 = require("react");
56
- var import_ink9 = require("ink");
57
- var import_node_fs2 = require("fs");
58
- var import_node_path2 = require("path");
184
+ var import_ink10 = require("ink");
185
+
186
+ // src/commands/slash-executor.ts
187
+ var VALID_MODES2 = ["plan", "default", "acceptEdits", "bypassPermissions"];
188
+ var HELP_TEXT = [
189
+ "Available commands:",
190
+ " /help \u2014 Show this help",
191
+ " /clear \u2014 Clear conversation",
192
+ " /compact [instr] \u2014 Compact context (optional focus instructions)",
193
+ " /mode [m] \u2014 Show/change permission mode",
194
+ " /cost \u2014 Show session info",
195
+ " /reset \u2014 Delete settings and exit",
196
+ " /exit \u2014 Exit CLI"
197
+ ].join("\n");
198
+ function handleHelp(addMessage) {
199
+ addMessage({ role: "system", content: HELP_TEXT });
200
+ return { handled: true };
201
+ }
202
+ function handleClear(addMessage, clearMessages, session) {
203
+ clearMessages();
204
+ session.clearHistory();
205
+ addMessage({ role: "system", content: "Conversation cleared." });
206
+ return { handled: true };
207
+ }
208
+ async function handleCompact(args, session, addMessage) {
209
+ const instructions = args.trim() || void 0;
210
+ const before = session.getContextState().usedPercentage;
211
+ addMessage({ role: "system", content: "Compacting context..." });
212
+ await session.compact(instructions);
213
+ const after = session.getContextState().usedPercentage;
214
+ addMessage({
215
+ role: "system",
216
+ content: `Context compacted: ${Math.round(before)}% -> ${Math.round(after)}%`
217
+ });
218
+ return { handled: true };
219
+ }
220
+ function handleMode(arg, session, addMessage) {
221
+ if (!arg) {
222
+ addMessage({ role: "system", content: `Current mode: ${session.getPermissionMode()}` });
223
+ } else if (VALID_MODES2.includes(arg)) {
224
+ session.setPermissionMode(arg);
225
+ addMessage({ role: "system", content: `Permission mode set to: ${arg}` });
226
+ } else {
227
+ addMessage({ role: "system", content: `Invalid mode. Valid: ${VALID_MODES2.join(" | ")}` });
228
+ }
229
+ return { handled: true };
230
+ }
231
+ function handleModel(modelId, addMessage) {
232
+ if (!modelId) {
233
+ addMessage({ role: "system", content: "Select a model from the /model submenu." });
234
+ return { handled: true };
235
+ }
236
+ return { handled: true, pendingModelId: modelId };
237
+ }
238
+ function handleCost(session, addMessage) {
239
+ addMessage({
240
+ role: "system",
241
+ content: `Session: ${session.getSessionId()}
242
+ Messages: ${session.getMessageCount()}`
243
+ });
244
+ return { handled: true };
245
+ }
246
+ function handlePermissions(session, addMessage) {
247
+ const mode = session.getPermissionMode();
248
+ const sessionAllowed = session.getSessionAllowedTools();
249
+ const lines = [`Permission mode: ${mode}`];
250
+ if (sessionAllowed.length > 0) {
251
+ lines.push(`Session-approved tools: ${sessionAllowed.join(", ")}`);
252
+ } else {
253
+ lines.push("No session-approved tools.");
254
+ }
255
+ addMessage({ role: "system", content: lines.join("\n") });
256
+ return { handled: true };
257
+ }
258
+ function handleContext(session, addMessage) {
259
+ const ctx = session.getContextState();
260
+ addMessage({
261
+ role: "system",
262
+ content: `Context: ${ctx.usedTokens.toLocaleString()} / ${ctx.maxTokens.toLocaleString()} tokens (${Math.round(ctx.usedPercentage)}%)`
263
+ });
264
+ return { handled: true };
265
+ }
266
+ function handleReset(addMessage) {
267
+ const settingsPath = getUserSettingsPath();
268
+ if (deleteSettings(settingsPath)) {
269
+ addMessage({ role: "system", content: `Deleted ${settingsPath}. Exiting...` });
270
+ } else {
271
+ addMessage({ role: "system", content: "No user settings found." });
272
+ }
273
+ return { handled: true, exitRequested: true };
274
+ }
275
+ async function executeSlashCommand(cmd, args, session, addMessage, clearMessages, registry) {
276
+ switch (cmd) {
277
+ case "help":
278
+ return handleHelp(addMessage);
279
+ case "clear":
280
+ return handleClear(addMessage, clearMessages, session);
281
+ case "compact":
282
+ return handleCompact(args, session, addMessage);
283
+ case "mode":
284
+ return handleMode(args.split(/\s+/)[0] || void 0, session, addMessage);
285
+ case "model":
286
+ return handleModel(args.split(/\s+/)[0] || void 0, addMessage);
287
+ case "cost":
288
+ return handleCost(session, addMessage);
289
+ case "permissions":
290
+ return handlePermissions(session, addMessage);
291
+ case "context":
292
+ return handleContext(session, addMessage);
293
+ case "reset":
294
+ return handleReset(addMessage);
295
+ case "exit":
296
+ return { handled: true, exitRequested: true };
297
+ default: {
298
+ const skillCmd = registry.getCommands().find((c) => c.name === cmd && c.source === "skill");
299
+ if (skillCmd) {
300
+ addMessage({ role: "system", content: `Invoking skill: ${cmd}` });
301
+ return { handled: false };
302
+ }
303
+ addMessage({ role: "system", content: `Unknown command "/${cmd}". Type /help for help.` });
304
+ return { handled: true };
305
+ }
306
+ }
307
+ }
308
+
309
+ // src/ui/App.tsx
59
310
  var import_agent_sdk = require("@robota-sdk/agent-sdk");
60
311
  var import_agent_core3 = require("@robota-sdk/agent-core");
61
312
 
@@ -146,8 +397,8 @@ var BuiltinCommandSource = class {
146
397
  };
147
398
 
148
399
  // src/commands/skill-source.ts
149
- var import_node_fs = require("fs");
150
- var import_node_path = require("path");
400
+ var import_node_fs2 = require("fs");
401
+ var import_node_path2 = require("path");
151
402
  var import_node_os = require("os");
152
403
  function parseFrontmatter(content) {
153
404
  const lines = content.split("\n");
@@ -170,14 +421,14 @@ function parseFrontmatter(content) {
170
421
  return name ? { name, description } : null;
171
422
  }
172
423
  function scanSkillsDir(skillsDir) {
173
- if (!(0, import_node_fs.existsSync)(skillsDir)) return [];
424
+ if (!(0, import_node_fs2.existsSync)(skillsDir)) return [];
174
425
  const commands = [];
175
- const entries = (0, import_node_fs.readdirSync)(skillsDir, { withFileTypes: true });
426
+ const entries = (0, import_node_fs2.readdirSync)(skillsDir, { withFileTypes: true });
176
427
  for (const entry of entries) {
177
428
  if (!entry.isDirectory()) continue;
178
- const skillFile = (0, import_node_path.join)(skillsDir, entry.name, "SKILL.md");
179
- if (!(0, import_node_fs.existsSync)(skillFile)) continue;
180
- const content = (0, import_node_fs.readFileSync)(skillFile, "utf-8");
429
+ const skillFile = (0, import_node_path2.join)(skillsDir, entry.name, "SKILL.md");
430
+ if (!(0, import_node_fs2.existsSync)(skillFile)) continue;
431
+ const content = (0, import_node_fs2.readFileSync)(skillFile, "utf-8");
181
432
  const frontmatter = parseFrontmatter(content);
182
433
  commands.push({
183
434
  name: frontmatter?.name ?? entry.name,
@@ -197,8 +448,8 @@ var SkillCommandSource = class {
197
448
  }
198
449
  getCommands() {
199
450
  if (this.cachedCommands) return this.cachedCommands;
200
- const projectSkills = scanSkillsDir((0, import_node_path.join)(this.cwd, ".agents", "skills"));
201
- const userSkills = scanSkillsDir((0, import_node_path.join)((0, import_node_os.homedir)(), ".claude", "skills"));
451
+ const projectSkills = scanSkillsDir((0, import_node_path2.join)(this.cwd, ".agents", "skills"));
452
+ const userSkills = scanSkillsDir((0, import_node_path2.join)((0, import_node_os.homedir)(), ".claude", "skills"));
202
453
  const seen = new Set(projectSkills.map((cmd) => cmd.name));
203
454
  const merged = [...projectSkills];
204
455
  for (const cmd of userSkills) {
@@ -740,8 +991,66 @@ function PermissionPrompt({ request }) {
740
991
  ] });
741
992
  }
742
993
 
743
- // src/ui/App.tsx
994
+ // src/utils/tool-call-extractor.ts
995
+ var TOOL_ARG_MAX_LENGTH = 80;
996
+ var TOOL_ARG_TRUNCATE_LENGTH = 77;
997
+ function extractToolCalls(history, startIndex) {
998
+ const lines = [];
999
+ for (let i = startIndex; i < history.length; i++) {
1000
+ const msg = history[i];
1001
+ if (msg.role === "assistant" && msg.toolCalls) {
1002
+ for (const tc of msg.toolCalls) {
1003
+ const value = parseFirstArgValue(tc.function.arguments);
1004
+ const truncated = value.length > TOOL_ARG_MAX_LENGTH ? value.slice(0, TOOL_ARG_TRUNCATE_LENGTH) + "..." : value;
1005
+ lines.push(`${tc.function.name}(${truncated})`);
1006
+ }
1007
+ }
1008
+ }
1009
+ return lines;
1010
+ }
1011
+ function parseFirstArgValue(argsJson) {
1012
+ try {
1013
+ const parsed = JSON.parse(argsJson);
1014
+ const firstVal = Object.values(parsed)[0];
1015
+ return typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal);
1016
+ } catch {
1017
+ return argsJson;
1018
+ }
1019
+ }
1020
+
1021
+ // src/ui/StreamingIndicator.tsx
1022
+ var import_ink9 = require("ink");
744
1023
  var import_jsx_runtime9 = require("react/jsx-runtime");
1024
+ function StreamingIndicator({ text, activeTools }) {
1025
+ const hasTools = activeTools.length > 0;
1026
+ const hasText = text.length > 0;
1027
+ if (!hasTools && !hasText) {
1028
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Text, { color: "yellow", children: "Thinking..." });
1029
+ }
1030
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink9.Box, { flexDirection: "column", children: [
1031
+ hasTools && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink9.Box, { flexDirection: "column", marginBottom: 1, children: [
1032
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Text, { color: "gray", bold: true, children: "Tools:" }),
1033
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Text, { children: " " }),
1034
+ activeTools.map((t, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink9.Text, { color: t.isRunning ? "yellow" : "green", children: [
1035
+ " ",
1036
+ t.isRunning ? "\u27F3" : "\u2713",
1037
+ " ",
1038
+ t.toolName,
1039
+ "(",
1040
+ t.firstArg,
1041
+ ")"
1042
+ ] }, `${t.toolName}-${i}`))
1043
+ ] }),
1044
+ hasText && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink9.Box, { flexDirection: "column", marginBottom: 1, children: [
1045
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Text, { color: "cyan", bold: true, children: "Robota:" }),
1046
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Text, { children: " " }),
1047
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Box, { marginLeft: 2, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Text, { wrap: "wrap", children: renderMarkdown(text) }) })
1048
+ ] })
1049
+ ] });
1050
+ }
1051
+
1052
+ // src/ui/App.tsx
1053
+ var import_jsx_runtime10 = require("react/jsx-runtime");
745
1054
  var msgIdCounter = 0;
746
1055
  function nextId() {
747
1056
  msgIdCounter += 1;
@@ -765,6 +1074,7 @@ var NOOP_TERMINAL = {
765
1074
  function useSession(props) {
766
1075
  const [permissionRequest, setPermissionRequest] = (0, import_react6.useState)(null);
767
1076
  const [streamingText, setStreamingText] = (0, import_react6.useState)("");
1077
+ const [activeTools, setActiveTools] = (0, import_react6.useState)([]);
768
1078
  const permissionQueueRef = (0, import_react6.useRef)([]);
769
1079
  const processingRef = (0, import_react6.useRef)(false);
770
1080
  const processNextPermission = (0, import_react6.useCallback)(() => {
@@ -798,6 +1108,25 @@ function useSession(props) {
798
1108
  const onTextDelta = (delta) => {
799
1109
  setStreamingText((prev) => prev + delta);
800
1110
  };
1111
+ const TOOL_ARG_DISPLAY_MAX = 80;
1112
+ const TOOL_ARG_TRUNCATE_AT = 77;
1113
+ const onToolExecution = (event) => {
1114
+ if (event.type === "start") {
1115
+ let firstArg = "";
1116
+ if (event.toolArgs) {
1117
+ const firstVal = Object.values(event.toolArgs)[0];
1118
+ const raw = typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal ?? "");
1119
+ firstArg = raw.length > TOOL_ARG_DISPLAY_MAX ? raw.slice(0, TOOL_ARG_TRUNCATE_AT) + "..." : raw;
1120
+ }
1121
+ setActiveTools((prev) => [...prev, { toolName: event.toolName, firstArg, isRunning: true }]);
1122
+ } else {
1123
+ setActiveTools(
1124
+ (prev) => prev.map(
1125
+ (t) => t.toolName === event.toolName && t.isRunning ? { ...t, isRunning: false } : t
1126
+ )
1127
+ );
1128
+ }
1129
+ };
801
1130
  const paths = (0, import_agent_sdk.projectPaths)(props.cwd ?? process.cwd());
802
1131
  sessionRef.current = (0, import_agent_sdk.createSession)({
803
1132
  config: props.config,
@@ -809,11 +1138,15 @@ function useSession(props) {
809
1138
  permissionMode: props.permissionMode,
810
1139
  maxTurns: props.maxTurns,
811
1140
  permissionHandler,
812
- onTextDelta
1141
+ onTextDelta,
1142
+ onToolExecution
813
1143
  });
814
1144
  }
815
- const clearStreamingText = (0, import_react6.useCallback)(() => setStreamingText(""), []);
816
- return { session: sessionRef.current, permissionRequest, streamingText, clearStreamingText };
1145
+ const clearStreamingText = (0, import_react6.useCallback)(() => {
1146
+ setStreamingText("");
1147
+ setActiveTools([]);
1148
+ }, []);
1149
+ return { session: sessionRef.current, permissionRequest, streamingText, clearStreamingText, activeTools };
817
1150
  }
818
1151
  function useMessages() {
819
1152
  const [messages, setMessages] = (0, import_react6.useState)([]);
@@ -822,139 +1155,32 @@ function useMessages() {
822
1155
  }, []);
823
1156
  return { messages, setMessages, addMessage };
824
1157
  }
825
- var HELP_TEXT = [
826
- "Available commands:",
827
- " /help \u2014 Show this help",
828
- " /clear \u2014 Clear conversation",
829
- " /compact [instr] \u2014 Compact context (optional focus instructions)",
830
- " /mode [m] \u2014 Show/change permission mode",
831
- " /cost \u2014 Show session info",
832
- " /reset \u2014 Delete settings and exit",
833
- " /exit \u2014 Exit CLI"
834
- ].join("\n");
835
- function handleModeCommand(arg, session, addMessage) {
836
- const validModes = ["plan", "default", "acceptEdits", "bypassPermissions"];
837
- if (!arg) {
838
- addMessage({ role: "system", content: `Current mode: ${session.getPermissionMode()}` });
839
- } else if (validModes.includes(arg)) {
840
- session.setPermissionMode(arg);
841
- addMessage({ role: "system", content: `Permission mode set to: ${arg}` });
842
- } else {
843
- addMessage({ role: "system", content: `Invalid mode. Valid: ${validModes.join(" | ")}` });
844
- }
845
- return true;
846
- }
847
- async function executeSlashCommand(cmd, parts, session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId) {
848
- switch (cmd) {
849
- case "help":
850
- addMessage({ role: "system", content: HELP_TEXT });
851
- return true;
852
- case "clear":
853
- setMessages([]);
854
- session.clearHistory();
855
- addMessage({ role: "system", content: "Conversation cleared." });
856
- return true;
857
- case "compact": {
858
- const instructions = parts.slice(1).join(" ").trim() || void 0;
859
- const before = session.getContextState().usedPercentage;
860
- addMessage({ role: "system", content: "Compacting context..." });
861
- await session.compact(instructions);
862
- const after = session.getContextState().usedPercentage;
863
- addMessage({
864
- role: "system",
865
- content: `Context compacted: ${Math.round(before)}% -> ${Math.round(after)}%`
866
- });
867
- return true;
868
- }
869
- case "mode":
870
- return handleModeCommand(parts[1], session, addMessage);
871
- case "model": {
872
- const modelId = parts[1];
873
- if (!modelId) {
874
- addMessage({ role: "system", content: "Select a model from the /model submenu." });
875
- return true;
876
- }
877
- pendingModelChangeRef.current = modelId;
878
- setPendingModelId(modelId);
879
- return true;
880
- }
881
- case "cost":
882
- addMessage({
883
- role: "system",
884
- content: `Session: ${session.getSessionId()}
885
- Messages: ${session.getMessageCount()}`
886
- });
887
- return true;
888
- case "permissions": {
889
- const mode = session.getPermissionMode();
890
- const sessionAllowed = session.getSessionAllowedTools();
891
- const lines = [`Permission mode: ${mode}`];
892
- if (sessionAllowed.length > 0) {
893
- lines.push(`Session-approved tools: ${sessionAllowed.join(", ")}`);
894
- } else {
895
- lines.push("No session-approved tools.");
896
- }
897
- addMessage({ role: "system", content: lines.join("\n") });
898
- return true;
899
- }
900
- case "context": {
901
- const ctx = session.getContextState();
902
- addMessage({
903
- role: "system",
904
- content: `Context: ${ctx.usedTokens.toLocaleString()} / ${ctx.maxTokens.toLocaleString()} tokens (${Math.round(ctx.usedPercentage)}%)`
905
- });
906
- return true;
907
- }
908
- case "reset": {
909
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
910
- const settingsPath = `${home}/.robota/settings.json`;
911
- if ((0, import_node_fs2.existsSync)(settingsPath)) {
912
- (0, import_node_fs2.unlinkSync)(settingsPath);
913
- addMessage({ role: "system", content: `Deleted ${settingsPath}. Exiting...` });
914
- } else {
915
- addMessage({ role: "system", content: "No user settings found." });
916
- }
917
- setTimeout(() => exit(), 500);
918
- return true;
919
- }
920
- case "exit":
921
- exit();
922
- return true;
923
- default: {
924
- const skillCmd = registry.getCommands().find((c) => c.name === cmd && c.source === "skill");
925
- if (skillCmd) {
926
- addMessage({ role: "system", content: `Invoking skill: ${cmd}` });
927
- return false;
928
- }
929
- addMessage({ role: "system", content: `Unknown command "/${cmd}". Type /help for help.` });
930
- return true;
931
- }
932
- }
933
- }
1158
+ var EXIT_DELAY_MS = 500;
934
1159
  function useSlashCommands(session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId) {
935
1160
  return (0, import_react6.useCallback)(
936
1161
  async (input) => {
937
1162
  const parts = input.slice(1).split(/\s+/);
938
1163
  const cmd = parts[0]?.toLowerCase() ?? "";
939
- return executeSlashCommand(cmd, parts, session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId);
1164
+ const args = parts.slice(1).join(" ");
1165
+ const clearMessages = () => setMessages([]);
1166
+ const result = await executeSlashCommand(cmd, args, session, addMessage, clearMessages, registry);
1167
+ if (result.pendingModelId) {
1168
+ pendingModelChangeRef.current = result.pendingModelId;
1169
+ setPendingModelId(result.pendingModelId);
1170
+ }
1171
+ if (result.exitRequested) {
1172
+ setTimeout(() => exit(), EXIT_DELAY_MS);
1173
+ }
1174
+ return result.handled;
940
1175
  },
941
1176
  [session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId]
942
1177
  );
943
1178
  }
944
- function StreamingIndicator({ text }) {
945
- if (text) {
946
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink9.Box, { flexDirection: "column", children: [
947
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink9.Text, { color: "cyan", bold: true, children: [
948
- "Robota:",
949
- " "
950
- ] }),
951
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Text, { children: " " }),
952
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Box, { marginLeft: 2, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Text, { wrap: "wrap", children: renderMarkdown(text) }) })
953
- ] });
954
- }
955
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Text, { color: "yellow", children: "Thinking..." });
1179
+ function syncContextState(session, setter) {
1180
+ const ctx = session.getContextState();
1181
+ setter({ percentage: ctx.usedPercentage, usedTokens: ctx.usedTokens, maxTokens: ctx.maxTokens });
956
1182
  }
957
- async function runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextPercentage) {
1183
+ async function runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextState) {
958
1184
  setIsThinking(true);
959
1185
  clearStreamingText();
960
1186
  const historyBefore = session.getHistory().length;
@@ -962,29 +1188,15 @@ async function runSessionPrompt(prompt, session, addMessage, clearStreamingText,
962
1188
  const response = await session.run(prompt);
963
1189
  clearStreamingText();
964
1190
  const history = session.getHistory();
965
- const toolLines = [];
966
- for (let i = historyBefore; i < history.length; i++) {
967
- const msg = history[i];
968
- if (msg.role === "assistant" && msg.toolCalls) {
969
- for (const tc of msg.toolCalls) {
970
- let value = "";
971
- try {
972
- const parsed = JSON.parse(tc.function.arguments);
973
- const firstVal = Object.values(parsed)[0];
974
- value = typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal);
975
- } catch {
976
- value = tc.function.arguments;
977
- }
978
- const truncated = value.length > 80 ? value.slice(0, 77) + "..." : value;
979
- toolLines.push(`${tc.function.name}(${truncated})`);
980
- }
981
- }
982
- }
1191
+ const toolLines = extractToolCalls(
1192
+ history,
1193
+ historyBefore
1194
+ );
983
1195
  if (toolLines.length > 0) {
984
1196
  addMessage({ role: "tool", content: toolLines.join("\n"), toolName: `${toolLines.length} tools` });
985
1197
  }
986
1198
  addMessage({ role: "assistant", content: response || "(empty response)" });
987
- setContextPercentage(session.getContextState().usedPercentage);
1199
+ syncContextState(session, setContextState);
988
1200
  } catch (err) {
989
1201
  clearStreamingText();
990
1202
  if (err instanceof DOMException && err.name === "AbortError") {
@@ -1013,13 +1225,13 @@ Execute the "${cmd}" skill: ${userInstruction}`;
1013
1225
  }
1014
1226
  return `Use the "${cmd}" skill: ${userInstruction}`;
1015
1227
  }
1016
- function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamingText, setIsThinking, setContextPercentage, registry) {
1228
+ function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamingText, setIsThinking, setContextState, registry) {
1017
1229
  return (0, import_react6.useCallback)(
1018
1230
  async (input) => {
1019
1231
  if (input.startsWith("/")) {
1020
1232
  const handled = await handleSlashCommand(input);
1021
1233
  if (handled) {
1022
- setContextPercentage(session.getContextState().usedPercentage);
1234
+ syncContextState(session, setContextState);
1023
1235
  return;
1024
1236
  }
1025
1237
  const prompt = buildSkillPrompt(input, registry);
@@ -1030,7 +1242,7 @@ function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamin
1030
1242
  addMessage,
1031
1243
  clearStreamingText,
1032
1244
  setIsThinking,
1033
- setContextPercentage
1245
+ setContextState
1034
1246
  );
1035
1247
  }
1036
1248
  addMessage({ role: "user", content: input });
@@ -1040,7 +1252,7 @@ function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamin
1040
1252
  addMessage,
1041
1253
  clearStreamingText,
1042
1254
  setIsThinking,
1043
- setContextPercentage
1255
+ setContextState
1044
1256
  );
1045
1257
  },
1046
1258
  [
@@ -1049,7 +1261,7 @@ function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamin
1049
1261
  handleSlashCommand,
1050
1262
  clearStreamingText,
1051
1263
  setIsThinking,
1052
- setContextPercentage,
1264
+ setContextState,
1053
1265
  registry
1054
1266
  ]
1055
1267
  );
@@ -1065,11 +1277,11 @@ function useCommandRegistry(cwd) {
1065
1277
  return registryRef.current;
1066
1278
  }
1067
1279
  function App(props) {
1068
- const { exit } = (0, import_ink9.useApp)();
1069
- const { session, permissionRequest, streamingText, clearStreamingText } = useSession(props);
1280
+ const { exit } = (0, import_ink10.useApp)();
1281
+ const { session, permissionRequest, streamingText, clearStreamingText, activeTools } = useSession(props);
1070
1282
  const { messages, setMessages, addMessage } = useMessages();
1071
1283
  const [isThinking, setIsThinking] = (0, import_react6.useState)(false);
1072
- const [contextPercentage, setContextPercentage] = (0, import_react6.useState)(0);
1284
+ const [contextState, setContextState] = (0, import_react6.useState)({ percentage: 0, usedTokens: 0, maxTokens: 0 });
1073
1285
  const registry = useCommandRegistry(props.cwd ?? process.cwd());
1074
1286
  const pendingModelChangeRef = (0, import_react6.useRef)(null);
1075
1287
  const [pendingModelId, setPendingModelId] = (0, import_react6.useState)(null);
@@ -1080,36 +1292,36 @@ function App(props) {
1080
1292
  handleSlashCommand,
1081
1293
  clearStreamingText,
1082
1294
  setIsThinking,
1083
- setContextPercentage,
1295
+ setContextState,
1084
1296
  registry
1085
1297
  );
1086
- (0, import_ink9.useInput)(
1298
+ (0, import_ink10.useInput)(
1087
1299
  (_input, key) => {
1088
1300
  if (key.ctrl && _input === "c") exit();
1089
1301
  if (key.escape && isThinking) session.abort();
1090
1302
  },
1091
1303
  { isActive: !permissionRequest }
1092
1304
  );
1093
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink9.Box, { flexDirection: "column", children: [
1094
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink9.Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1095
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Text, { color: "cyan", bold: true, children: `
1305
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_ink10.Box, { flexDirection: "column", children: [
1306
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_ink10.Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1307
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_ink10.Text, { color: "cyan", bold: true, children: `
1096
1308
  ____ ___ ____ ___ _____ _
1097
1309
  | _ \\ / _ \\| __ ) / _ \\_ _|/ \\
1098
1310
  | |_) | | | | _ \\| | | || | / _ \\
1099
1311
  | _ <| |_| | |_) | |_| || |/ ___ \\
1100
1312
  |_| \\_\\\\___/|____/ \\___/ |_/_/ \\_\\
1101
1313
  ` }),
1102
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink9.Text, { dimColor: true, children: [
1314
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_ink10.Text, { dimColor: true, children: [
1103
1315
  " v",
1104
1316
  props.version ?? "0.0.0"
1105
1317
  ] })
1106
1318
  ] }),
1107
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink9.Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
1108
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(MessageList, { messages }),
1109
- isThinking && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Box, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StreamingIndicator, { text: streamingText }) })
1319
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_ink10.Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
1320
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessageList, { messages }),
1321
+ isThinking && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_ink10.Box, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StreamingIndicator, { text: streamingText, activeTools }) })
1110
1322
  ] }),
1111
- permissionRequest && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PermissionPrompt, { request: permissionRequest }),
1112
- pendingModelId && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1323
+ permissionRequest && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PermissionPrompt, { request: permissionRequest }),
1324
+ pendingModelId && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1113
1325
  ConfirmPrompt,
1114
1326
  {
1115
1327
  message: `Change model to ${(0, import_agent_core3.getModelName)(pendingModelId)}? This will restart the session.`,
@@ -1118,17 +1330,8 @@ function App(props) {
1118
1330
  pendingModelChangeRef.current = null;
1119
1331
  if (index === 0) {
1120
1332
  try {
1121
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
1122
- const settingsPath = (0, import_node_path2.join)(home, ".robota", "settings.json");
1123
- let settings = {};
1124
- if ((0, import_node_fs2.existsSync)(settingsPath)) {
1125
- settings = JSON.parse((0, import_node_fs2.readFileSync)(settingsPath, "utf8"));
1126
- }
1127
- const provider = settings.provider ?? {};
1128
- provider.model = pendingModelId;
1129
- settings.provider = provider;
1130
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.join)(home, ".robota"), { recursive: true });
1131
- (0, import_node_fs2.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1333
+ const settingsPath = getUserSettingsPath();
1334
+ updateModelInSettings(settingsPath, pendingModelId);
1132
1335
  addMessage({ role: "system", content: `Model changed to ${(0, import_agent_core3.getModelName)(pendingModelId)}. Restarting...` });
1133
1336
  setTimeout(() => exit(), 500);
1134
1337
  } catch (err) {
@@ -1140,7 +1343,7 @@ function App(props) {
1140
1343
  }
1141
1344
  }
1142
1345
  ),
1143
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1346
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1144
1347
  StatusBar,
1145
1348
  {
1146
1349
  permissionMode: session.getPermissionMode(),
@@ -1148,12 +1351,12 @@ function App(props) {
1148
1351
  sessionId: session.getSessionId(),
1149
1352
  messageCount: messages.length,
1150
1353
  isThinking,
1151
- contextPercentage,
1152
- contextUsedTokens: session.getContextState().usedTokens,
1153
- contextMaxTokens: session.getContextState().maxTokens
1354
+ contextPercentage: contextState.percentage,
1355
+ contextUsedTokens: contextState.usedTokens,
1356
+ contextMaxTokens: contextState.maxTokens
1154
1357
  }
1155
1358
  ),
1156
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1359
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1157
1360
  InputArea,
1158
1361
  {
1159
1362
  onSubmit: handleSubmit,
@@ -1161,12 +1364,12 @@ function App(props) {
1161
1364
  registry
1162
1365
  }
1163
1366
  ),
1164
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink9.Text, { children: " " })
1367
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_ink10.Text, { children: " " })
1165
1368
  ] });
1166
1369
  }
1167
1370
 
1168
1371
  // src/ui/render.tsx
1169
- var import_jsx_runtime10 = require("react/jsx-runtime");
1372
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1170
1373
  function renderApp(options) {
1171
1374
  process.on("unhandledRejection", (reason) => {
1172
1375
  process.stderr.write(`
@@ -1177,7 +1380,7 @@ function renderApp(options) {
1177
1380
  `);
1178
1381
  }
1179
1382
  });
1180
- const instance = (0, import_ink10.render)(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(App, { ...options }), {
1383
+ const instance = (0, import_ink11.render)(/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(App, { ...options }), {
1181
1384
  exitOnCtrlC: true
1182
1385
  });
1183
1386
  instance.waitUntilExit().catch((err) => {
@@ -1191,7 +1394,18 @@ function renderApp(options) {
1191
1394
 
1192
1395
  // src/cli.ts
1193
1396
  var import_meta = {};
1194
- var VALID_MODES = ["plan", "default", "acceptEdits", "bypassPermissions"];
1397
+ function hasValidSettingsFile(filePath) {
1398
+ if (!(0, import_node_fs3.existsSync)(filePath)) return false;
1399
+ try {
1400
+ const raw = (0, import_node_fs3.readFileSync)(filePath, "utf8").trim();
1401
+ if (raw.length === 0) return false;
1402
+ const parsed = JSON.parse(raw);
1403
+ const provider = parsed.provider;
1404
+ return !!provider?.apiKey;
1405
+ } catch {
1406
+ return false;
1407
+ }
1408
+ }
1195
1409
  function readVersion() {
1196
1410
  try {
1197
1411
  const thisFile = (0, import_node_url.fileURLToPath)(import_meta.url);
@@ -1212,108 +1426,11 @@ function readVersion() {
1212
1426
  return "0.0.0";
1213
1427
  }
1214
1428
  }
1215
- function parsePermissionMode(raw) {
1216
- if (raw === void 0) return void 0;
1217
- if (!VALID_MODES.includes(raw)) {
1218
- process.stderr.write(`Invalid --permission-mode "${raw}". Valid: ${VALID_MODES.join(" | ")}
1219
- `);
1220
- process.exit(1);
1221
- }
1222
- return raw;
1223
- }
1224
- function parseMaxTurns(raw) {
1225
- if (raw === void 0) return void 0;
1226
- const n = parseInt(raw, 10);
1227
- if (isNaN(n) || n <= 0) {
1228
- process.stderr.write(`Invalid --max-turns "${raw}". Must be a positive integer.
1229
- `);
1230
- process.exit(1);
1231
- }
1232
- return n;
1233
- }
1234
- function parseCliArgs() {
1235
- const { values, positionals } = (0, import_node_util.parseArgs)({
1236
- allowPositionals: true,
1237
- options: {
1238
- p: { type: "boolean", short: "p", default: false },
1239
- c: { type: "boolean", short: "c", default: false },
1240
- r: { type: "string", short: "r" },
1241
- model: { type: "string" },
1242
- "permission-mode": { type: "string" },
1243
- "max-turns": { type: "string" },
1244
- version: { type: "boolean", default: false },
1245
- reset: { type: "boolean", default: false }
1246
- }
1247
- });
1248
- return {
1249
- positional: positionals,
1250
- printMode: values["p"] ?? false,
1251
- continueMode: values["c"] ?? false,
1252
- resumeId: values["r"],
1253
- model: values["model"],
1254
- permissionMode: parsePermissionMode(values["permission-mode"]),
1255
- maxTurns: parseMaxTurns(values["max-turns"]),
1256
- version: values["version"] ?? false,
1257
- reset: values["reset"] ?? false
1258
- };
1259
- }
1260
- var PrintTerminal = class {
1261
- write(text) {
1262
- process.stdout.write(text);
1263
- }
1264
- writeLine(text) {
1265
- process.stdout.write(text + "\n");
1266
- }
1267
- writeMarkdown(md) {
1268
- process.stdout.write(md);
1269
- }
1270
- writeError(text) {
1271
- process.stderr.write(text + "\n");
1272
- }
1273
- prompt(question) {
1274
- return new Promise((resolve) => {
1275
- const rl = readline.createInterface({
1276
- input: process.stdin,
1277
- output: process.stdout,
1278
- terminal: false,
1279
- historySize: 0
1280
- });
1281
- rl.question(question, (answer) => {
1282
- rl.close();
1283
- resolve(answer);
1284
- });
1285
- });
1286
- }
1287
- async select(options, initialIndex = 0) {
1288
- for (let i = 0; i < options.length; i++) {
1289
- const marker = i === initialIndex ? ">" : " ";
1290
- process.stdout.write(` ${marker} ${i + 1}) ${options[i]}
1291
- `);
1292
- }
1293
- const answer = await this.prompt(
1294
- ` Choose [1-${options.length}] (default: ${options[initialIndex]}): `
1295
- );
1296
- const trimmed = answer.trim().toLowerCase();
1297
- if (trimmed === "") return initialIndex;
1298
- const num = parseInt(trimmed, 10);
1299
- if (!isNaN(num) && num >= 1 && num <= options.length) return num - 1;
1300
- return initialIndex;
1301
- }
1302
- spinner(_message) {
1303
- return { stop() {
1304
- }, update() {
1305
- } };
1306
- }
1307
- };
1308
- function getUserSettingsPath() {
1309
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
1310
- return (0, import_node_path3.join)(home, ".robota", "settings.json");
1311
- }
1312
1429
  async function ensureConfig(cwd) {
1313
1430
  const userPath = getUserSettingsPath();
1314
1431
  const projectPath = (0, import_node_path3.join)(cwd, ".robota", "settings.json");
1315
1432
  const localPath = (0, import_node_path3.join)(cwd, ".robota", "settings.local.json");
1316
- if ((0, import_node_fs3.existsSync)(userPath) || (0, import_node_fs3.existsSync)(projectPath) || (0, import_node_fs3.existsSync)(localPath)) {
1433
+ if (hasValidSettingsFile(userPath) || hasValidSettingsFile(projectPath) || hasValidSettingsFile(localPath)) {
1317
1434
  return;
1318
1435
  }
1319
1436
  process.stdout.write("\n");
@@ -1374,8 +1491,7 @@ async function ensureConfig(cwd) {
1374
1491
  }
1375
1492
  function resetConfig() {
1376
1493
  const userPath = getUserSettingsPath();
1377
- if ((0, import_node_fs3.existsSync)(userPath)) {
1378
- (0, import_node_fs3.unlinkSync)(userPath);
1494
+ if (deleteSettings(userPath)) {
1379
1495
  process.stdout.write(`Deleted ${userPath}
1380
1496
  `);
1381
1497
  } else {