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