@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.
@@ -1,9 +1,7 @@
1
1
  // src/cli.ts
2
- import { parseArgs } from "util";
3
- import { readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
4
- import { join as join3, dirname } from "path";
2
+ import { readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
3
+ import { join as join3, dirname as dirname2 } from "path";
5
4
  import { fileURLToPath } from "url";
6
- import * as readline from "readline";
7
5
  import {
8
6
  loadConfig,
9
7
  loadContext,
@@ -15,14 +13,267 @@ import {
15
13
  } from "@robota-sdk/agent-sdk";
16
14
  import { promptForApproval } from "@robota-sdk/agent-sdk";
17
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
+ "permission-mode": { type: "string" },
47
+ "max-turns": { type: "string" },
48
+ version: { type: "boolean", default: false },
49
+ reset: { type: "boolean", default: false }
50
+ }
51
+ });
52
+ return {
53
+ positional: positionals,
54
+ printMode: values["p"] ?? false,
55
+ continueMode: values["c"] ?? false,
56
+ resumeId: values["r"],
57
+ model: values["model"],
58
+ permissionMode: parsePermissionMode(values["permission-mode"]),
59
+ maxTurns: parseMaxTurns(values["max-turns"]),
60
+ version: values["version"] ?? false,
61
+ reset: values["reset"] ?? false
62
+ };
63
+ }
64
+
65
+ // src/utils/settings-io.ts
66
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
67
+ import { join, dirname } from "path";
68
+ function getUserSettingsPath() {
69
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
70
+ return join(home, ".robota", "settings.json");
71
+ }
72
+ function readSettings(path) {
73
+ if (!existsSync(path)) return {};
74
+ return JSON.parse(readFileSync(path, "utf8"));
75
+ }
76
+ function writeSettings(path, settings) {
77
+ mkdirSync(dirname(path), { recursive: true });
78
+ writeFileSync(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
79
+ }
80
+ function updateModelInSettings(settingsPath, modelId) {
81
+ const settings = readSettings(settingsPath);
82
+ const provider = settings.provider ?? {};
83
+ provider.model = modelId;
84
+ settings.provider = provider;
85
+ writeSettings(settingsPath, settings);
86
+ }
87
+ function deleteSettings(path) {
88
+ if (existsSync(path)) {
89
+ unlinkSync(path);
90
+ return true;
91
+ }
92
+ return false;
93
+ }
94
+
95
+ // src/print-terminal.ts
96
+ import * as readline from "readline";
97
+ var PrintTerminal = class {
98
+ write(text) {
99
+ process.stdout.write(text);
100
+ }
101
+ writeLine(text) {
102
+ process.stdout.write(text + "\n");
103
+ }
104
+ writeMarkdown(md) {
105
+ process.stdout.write(md);
106
+ }
107
+ writeError(text) {
108
+ process.stderr.write(text + "\n");
109
+ }
110
+ prompt(question) {
111
+ return new Promise((resolve) => {
112
+ const rl = readline.createInterface({
113
+ input: process.stdin,
114
+ output: process.stdout,
115
+ terminal: false,
116
+ historySize: 0
117
+ });
118
+ rl.question(question, (answer) => {
119
+ rl.close();
120
+ resolve(answer);
121
+ });
122
+ });
123
+ }
124
+ async select(options, initialIndex = 0) {
125
+ for (let i = 0; i < options.length; i++) {
126
+ const marker = i === initialIndex ? ">" : " ";
127
+ process.stdout.write(` ${marker} ${i + 1}) ${options[i]}
128
+ `);
129
+ }
130
+ const answer = await this.prompt(
131
+ ` Choose [1-${options.length}] (default: ${options[initialIndex]}): `
132
+ );
133
+ const trimmed = answer.trim().toLowerCase();
134
+ if (trimmed === "") return initialIndex;
135
+ const num = parseInt(trimmed, 10);
136
+ if (!isNaN(num) && num >= 1 && num <= options.length) return num - 1;
137
+ return initialIndex;
138
+ }
139
+ spinner(_message) {
140
+ return { stop() {
141
+ }, update() {
142
+ } };
143
+ }
144
+ };
145
+
18
146
  // src/ui/render.tsx
19
147
  import { render } from "ink";
20
148
 
21
149
  // src/ui/App.tsx
22
150
  import { useState as useState5, useCallback as useCallback3, useRef as useRef3 } from "react";
23
- import { Box as Box7, Text as Text9, useApp, useInput as useInput5 } from "ink";
24
- import { existsSync as existsSync2, unlinkSync, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
25
- import { join as join2 } from "path";
151
+ import { Box as Box8, Text as Text10, useApp, useInput as useInput5 } from "ink";
152
+
153
+ // src/commands/slash-executor.ts
154
+ var VALID_MODES2 = ["plan", "default", "acceptEdits", "bypassPermissions"];
155
+ var HELP_TEXT = [
156
+ "Available commands:",
157
+ " /help \u2014 Show this help",
158
+ " /clear \u2014 Clear conversation",
159
+ " /compact [instr] \u2014 Compact context (optional focus instructions)",
160
+ " /mode [m] \u2014 Show/change permission mode",
161
+ " /cost \u2014 Show session info",
162
+ " /reset \u2014 Delete settings and exit",
163
+ " /exit \u2014 Exit CLI"
164
+ ].join("\n");
165
+ function handleHelp(addMessage) {
166
+ addMessage({ role: "system", content: HELP_TEXT });
167
+ return { handled: true };
168
+ }
169
+ function handleClear(addMessage, clearMessages, session) {
170
+ clearMessages();
171
+ session.clearHistory();
172
+ addMessage({ role: "system", content: "Conversation cleared." });
173
+ return { handled: true };
174
+ }
175
+ async function handleCompact(args, session, addMessage) {
176
+ const instructions = args.trim() || void 0;
177
+ const before = session.getContextState().usedPercentage;
178
+ addMessage({ role: "system", content: "Compacting context..." });
179
+ await session.compact(instructions);
180
+ const after = session.getContextState().usedPercentage;
181
+ addMessage({
182
+ role: "system",
183
+ content: `Context compacted: ${Math.round(before)}% -> ${Math.round(after)}%`
184
+ });
185
+ return { handled: true };
186
+ }
187
+ function handleMode(arg, session, addMessage) {
188
+ if (!arg) {
189
+ addMessage({ role: "system", content: `Current mode: ${session.getPermissionMode()}` });
190
+ } else if (VALID_MODES2.includes(arg)) {
191
+ session.setPermissionMode(arg);
192
+ addMessage({ role: "system", content: `Permission mode set to: ${arg}` });
193
+ } else {
194
+ addMessage({ role: "system", content: `Invalid mode. Valid: ${VALID_MODES2.join(" | ")}` });
195
+ }
196
+ return { handled: true };
197
+ }
198
+ function handleModel(modelId, addMessage) {
199
+ if (!modelId) {
200
+ addMessage({ role: "system", content: "Select a model from the /model submenu." });
201
+ return { handled: true };
202
+ }
203
+ return { handled: true, pendingModelId: modelId };
204
+ }
205
+ function handleCost(session, addMessage) {
206
+ addMessage({
207
+ role: "system",
208
+ content: `Session: ${session.getSessionId()}
209
+ Messages: ${session.getMessageCount()}`
210
+ });
211
+ return { handled: true };
212
+ }
213
+ function handlePermissions(session, addMessage) {
214
+ const mode = session.getPermissionMode();
215
+ const sessionAllowed = session.getSessionAllowedTools();
216
+ const lines = [`Permission mode: ${mode}`];
217
+ if (sessionAllowed.length > 0) {
218
+ lines.push(`Session-approved tools: ${sessionAllowed.join(", ")}`);
219
+ } else {
220
+ lines.push("No session-approved tools.");
221
+ }
222
+ addMessage({ role: "system", content: lines.join("\n") });
223
+ return { handled: true };
224
+ }
225
+ function handleContext(session, addMessage) {
226
+ const ctx = session.getContextState();
227
+ addMessage({
228
+ role: "system",
229
+ content: `Context: ${ctx.usedTokens.toLocaleString()} / ${ctx.maxTokens.toLocaleString()} tokens (${Math.round(ctx.usedPercentage)}%)`
230
+ });
231
+ return { handled: true };
232
+ }
233
+ function handleReset(addMessage) {
234
+ const settingsPath = getUserSettingsPath();
235
+ if (deleteSettings(settingsPath)) {
236
+ addMessage({ role: "system", content: `Deleted ${settingsPath}. Exiting...` });
237
+ } else {
238
+ addMessage({ role: "system", content: "No user settings found." });
239
+ }
240
+ return { handled: true, exitRequested: true };
241
+ }
242
+ async function executeSlashCommand(cmd, args, session, addMessage, clearMessages, registry) {
243
+ switch (cmd) {
244
+ case "help":
245
+ return handleHelp(addMessage);
246
+ case "clear":
247
+ return handleClear(addMessage, clearMessages, session);
248
+ case "compact":
249
+ return handleCompact(args, session, addMessage);
250
+ case "mode":
251
+ return handleMode(args.split(/\s+/)[0] || void 0, session, addMessage);
252
+ case "model":
253
+ return handleModel(args.split(/\s+/)[0] || void 0, addMessage);
254
+ case "cost":
255
+ return handleCost(session, addMessage);
256
+ case "permissions":
257
+ return handlePermissions(session, addMessage);
258
+ case "context":
259
+ return handleContext(session, addMessage);
260
+ case "reset":
261
+ return handleReset(addMessage);
262
+ case "exit":
263
+ return { handled: true, exitRequested: true };
264
+ default: {
265
+ const skillCmd = registry.getCommands().find((c) => c.name === cmd && c.source === "skill");
266
+ if (skillCmd) {
267
+ addMessage({ role: "system", content: `Invoking skill: ${cmd}` });
268
+ return { handled: false };
269
+ }
270
+ addMessage({ role: "system", content: `Unknown command "/${cmd}". Type /help for help.` });
271
+ return { handled: true };
272
+ }
273
+ }
274
+ }
275
+
276
+ // src/ui/App.tsx
26
277
  import { createSession, FileSessionLogger, projectPaths } from "@robota-sdk/agent-sdk";
27
278
  import { getModelName } from "@robota-sdk/agent-core";
28
279
 
@@ -113,8 +364,8 @@ var BuiltinCommandSource = class {
113
364
  };
114
365
 
115
366
  // src/commands/skill-source.ts
116
- import { readdirSync, readFileSync, existsSync } from "fs";
117
- import { join } from "path";
367
+ import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
368
+ import { join as join2 } from "path";
118
369
  import { homedir } from "os";
119
370
  function parseFrontmatter(content) {
120
371
  const lines = content.split("\n");
@@ -137,14 +388,14 @@ function parseFrontmatter(content) {
137
388
  return name ? { name, description } : null;
138
389
  }
139
390
  function scanSkillsDir(skillsDir) {
140
- if (!existsSync(skillsDir)) return [];
391
+ if (!existsSync2(skillsDir)) return [];
141
392
  const commands = [];
142
393
  const entries = readdirSync(skillsDir, { withFileTypes: true });
143
394
  for (const entry of entries) {
144
395
  if (!entry.isDirectory()) continue;
145
- const skillFile = join(skillsDir, entry.name, "SKILL.md");
146
- if (!existsSync(skillFile)) continue;
147
- const content = readFileSync(skillFile, "utf-8");
396
+ const skillFile = join2(skillsDir, entry.name, "SKILL.md");
397
+ if (!existsSync2(skillFile)) continue;
398
+ const content = readFileSync2(skillFile, "utf-8");
148
399
  const frontmatter = parseFrontmatter(content);
149
400
  commands.push({
150
401
  name: frontmatter?.name ?? entry.name,
@@ -164,8 +415,8 @@ var SkillCommandSource = class {
164
415
  }
165
416
  getCommands() {
166
417
  if (this.cachedCommands) return this.cachedCommands;
167
- const projectSkills = scanSkillsDir(join(this.cwd, ".agents", "skills"));
168
- const userSkills = scanSkillsDir(join(homedir(), ".claude", "skills"));
418
+ const projectSkills = scanSkillsDir(join2(this.cwd, ".agents", "skills"));
419
+ const userSkills = scanSkillsDir(join2(homedir(), ".claude", "skills"));
169
420
  const seen = new Set(projectSkills.map((cmd) => cmd.name));
170
421
  const merged = [...projectSkills];
171
422
  for (const cmd of userSkills) {
@@ -707,8 +958,66 @@ function PermissionPrompt({ request }) {
707
958
  ] });
708
959
  }
709
960
 
710
- // src/ui/App.tsx
961
+ // src/utils/tool-call-extractor.ts
962
+ var TOOL_ARG_MAX_LENGTH = 80;
963
+ var TOOL_ARG_TRUNCATE_LENGTH = 77;
964
+ function extractToolCalls(history, startIndex) {
965
+ const lines = [];
966
+ for (let i = startIndex; i < history.length; i++) {
967
+ const msg = history[i];
968
+ if (msg.role === "assistant" && msg.toolCalls) {
969
+ for (const tc of msg.toolCalls) {
970
+ const value = parseFirstArgValue(tc.function.arguments);
971
+ const truncated = value.length > TOOL_ARG_MAX_LENGTH ? value.slice(0, TOOL_ARG_TRUNCATE_LENGTH) + "..." : value;
972
+ lines.push(`${tc.function.name}(${truncated})`);
973
+ }
974
+ }
975
+ }
976
+ return lines;
977
+ }
978
+ function parseFirstArgValue(argsJson) {
979
+ try {
980
+ const parsed = JSON.parse(argsJson);
981
+ const firstVal = Object.values(parsed)[0];
982
+ return typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal);
983
+ } catch {
984
+ return argsJson;
985
+ }
986
+ }
987
+
988
+ // src/ui/StreamingIndicator.tsx
989
+ import { Box as Box7, Text as Text9 } from "ink";
711
990
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
991
+ function StreamingIndicator({ text, activeTools }) {
992
+ const hasTools = activeTools.length > 0;
993
+ const hasText = text.length > 0;
994
+ if (!hasTools && !hasText) {
995
+ return /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Thinking..." });
996
+ }
997
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
998
+ hasTools && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
999
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", bold: true, children: "Tools:" }),
1000
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
1001
+ activeTools.map((t, i) => /* @__PURE__ */ jsxs7(Text9, { color: t.isRunning ? "yellow" : "green", children: [
1002
+ " ",
1003
+ t.isRunning ? "\u27F3" : "\u2713",
1004
+ " ",
1005
+ t.toolName,
1006
+ "(",
1007
+ t.firstArg,
1008
+ ")"
1009
+ ] }, `${t.toolName}-${i}`))
1010
+ ] }),
1011
+ hasText && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
1012
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: "Robota:" }),
1013
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
1014
+ /* @__PURE__ */ jsx9(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { wrap: "wrap", children: renderMarkdown(text) }) })
1015
+ ] })
1016
+ ] });
1017
+ }
1018
+
1019
+ // src/ui/App.tsx
1020
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
712
1021
  var msgIdCounter = 0;
713
1022
  function nextId() {
714
1023
  msgIdCounter += 1;
@@ -732,6 +1041,7 @@ var NOOP_TERMINAL = {
732
1041
  function useSession(props) {
733
1042
  const [permissionRequest, setPermissionRequest] = useState5(null);
734
1043
  const [streamingText, setStreamingText] = useState5("");
1044
+ const [activeTools, setActiveTools] = useState5([]);
735
1045
  const permissionQueueRef = useRef3([]);
736
1046
  const processingRef = useRef3(false);
737
1047
  const processNextPermission = useCallback3(() => {
@@ -765,6 +1075,25 @@ function useSession(props) {
765
1075
  const onTextDelta = (delta) => {
766
1076
  setStreamingText((prev) => prev + delta);
767
1077
  };
1078
+ const TOOL_ARG_DISPLAY_MAX = 80;
1079
+ const TOOL_ARG_TRUNCATE_AT = 77;
1080
+ const onToolExecution = (event) => {
1081
+ if (event.type === "start") {
1082
+ let firstArg = "";
1083
+ if (event.toolArgs) {
1084
+ const firstVal = Object.values(event.toolArgs)[0];
1085
+ const raw = typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal ?? "");
1086
+ firstArg = raw.length > TOOL_ARG_DISPLAY_MAX ? raw.slice(0, TOOL_ARG_TRUNCATE_AT) + "..." : raw;
1087
+ }
1088
+ setActiveTools((prev) => [...prev, { toolName: event.toolName, firstArg, isRunning: true }]);
1089
+ } else {
1090
+ setActiveTools(
1091
+ (prev) => prev.map(
1092
+ (t) => t.toolName === event.toolName && t.isRunning ? { ...t, isRunning: false } : t
1093
+ )
1094
+ );
1095
+ }
1096
+ };
768
1097
  const paths = projectPaths(props.cwd ?? process.cwd());
769
1098
  sessionRef.current = createSession({
770
1099
  config: props.config,
@@ -776,11 +1105,15 @@ function useSession(props) {
776
1105
  permissionMode: props.permissionMode,
777
1106
  maxTurns: props.maxTurns,
778
1107
  permissionHandler,
779
- onTextDelta
1108
+ onTextDelta,
1109
+ onToolExecution
780
1110
  });
781
1111
  }
782
- const clearStreamingText = useCallback3(() => setStreamingText(""), []);
783
- return { session: sessionRef.current, permissionRequest, streamingText, clearStreamingText };
1112
+ const clearStreamingText = useCallback3(() => {
1113
+ setStreamingText("");
1114
+ setActiveTools([]);
1115
+ }, []);
1116
+ return { session: sessionRef.current, permissionRequest, streamingText, clearStreamingText, activeTools };
784
1117
  }
785
1118
  function useMessages() {
786
1119
  const [messages, setMessages] = useState5([]);
@@ -789,139 +1122,32 @@ function useMessages() {
789
1122
  }, []);
790
1123
  return { messages, setMessages, addMessage };
791
1124
  }
792
- var HELP_TEXT = [
793
- "Available commands:",
794
- " /help \u2014 Show this help",
795
- " /clear \u2014 Clear conversation",
796
- " /compact [instr] \u2014 Compact context (optional focus instructions)",
797
- " /mode [m] \u2014 Show/change permission mode",
798
- " /cost \u2014 Show session info",
799
- " /reset \u2014 Delete settings and exit",
800
- " /exit \u2014 Exit CLI"
801
- ].join("\n");
802
- function handleModeCommand(arg, session, addMessage) {
803
- const validModes = ["plan", "default", "acceptEdits", "bypassPermissions"];
804
- if (!arg) {
805
- addMessage({ role: "system", content: `Current mode: ${session.getPermissionMode()}` });
806
- } else if (validModes.includes(arg)) {
807
- session.setPermissionMode(arg);
808
- addMessage({ role: "system", content: `Permission mode set to: ${arg}` });
809
- } else {
810
- addMessage({ role: "system", content: `Invalid mode. Valid: ${validModes.join(" | ")}` });
811
- }
812
- return true;
813
- }
814
- async function executeSlashCommand(cmd, parts, session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId) {
815
- switch (cmd) {
816
- case "help":
817
- addMessage({ role: "system", content: HELP_TEXT });
818
- return true;
819
- case "clear":
820
- setMessages([]);
821
- session.clearHistory();
822
- addMessage({ role: "system", content: "Conversation cleared." });
823
- return true;
824
- case "compact": {
825
- const instructions = parts.slice(1).join(" ").trim() || void 0;
826
- const before = session.getContextState().usedPercentage;
827
- addMessage({ role: "system", content: "Compacting context..." });
828
- await session.compact(instructions);
829
- const after = session.getContextState().usedPercentage;
830
- addMessage({
831
- role: "system",
832
- content: `Context compacted: ${Math.round(before)}% -> ${Math.round(after)}%`
833
- });
834
- return true;
835
- }
836
- case "mode":
837
- return handleModeCommand(parts[1], session, addMessage);
838
- case "model": {
839
- const modelId = parts[1];
840
- if (!modelId) {
841
- addMessage({ role: "system", content: "Select a model from the /model submenu." });
842
- return true;
843
- }
844
- pendingModelChangeRef.current = modelId;
845
- setPendingModelId(modelId);
846
- return true;
847
- }
848
- case "cost":
849
- addMessage({
850
- role: "system",
851
- content: `Session: ${session.getSessionId()}
852
- Messages: ${session.getMessageCount()}`
853
- });
854
- return true;
855
- case "permissions": {
856
- const mode = session.getPermissionMode();
857
- const sessionAllowed = session.getSessionAllowedTools();
858
- const lines = [`Permission mode: ${mode}`];
859
- if (sessionAllowed.length > 0) {
860
- lines.push(`Session-approved tools: ${sessionAllowed.join(", ")}`);
861
- } else {
862
- lines.push("No session-approved tools.");
863
- }
864
- addMessage({ role: "system", content: lines.join("\n") });
865
- return true;
866
- }
867
- case "context": {
868
- const ctx = session.getContextState();
869
- addMessage({
870
- role: "system",
871
- content: `Context: ${ctx.usedTokens.toLocaleString()} / ${ctx.maxTokens.toLocaleString()} tokens (${Math.round(ctx.usedPercentage)}%)`
872
- });
873
- return true;
874
- }
875
- case "reset": {
876
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
877
- const settingsPath = `${home}/.robota/settings.json`;
878
- if (existsSync2(settingsPath)) {
879
- unlinkSync(settingsPath);
880
- addMessage({ role: "system", content: `Deleted ${settingsPath}. Exiting...` });
881
- } else {
882
- addMessage({ role: "system", content: "No user settings found." });
883
- }
884
- setTimeout(() => exit(), 500);
885
- return true;
886
- }
887
- case "exit":
888
- exit();
889
- return true;
890
- default: {
891
- const skillCmd = registry.getCommands().find((c) => c.name === cmd && c.source === "skill");
892
- if (skillCmd) {
893
- addMessage({ role: "system", content: `Invoking skill: ${cmd}` });
894
- return false;
895
- }
896
- addMessage({ role: "system", content: `Unknown command "/${cmd}". Type /help for help.` });
897
- return true;
898
- }
899
- }
900
- }
1125
+ var EXIT_DELAY_MS = 500;
901
1126
  function useSlashCommands(session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId) {
902
1127
  return useCallback3(
903
1128
  async (input) => {
904
1129
  const parts = input.slice(1).split(/\s+/);
905
1130
  const cmd = parts[0]?.toLowerCase() ?? "";
906
- return executeSlashCommand(cmd, parts, session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId);
1131
+ const args = parts.slice(1).join(" ");
1132
+ const clearMessages = () => setMessages([]);
1133
+ const result = await executeSlashCommand(cmd, args, session, addMessage, clearMessages, registry);
1134
+ if (result.pendingModelId) {
1135
+ pendingModelChangeRef.current = result.pendingModelId;
1136
+ setPendingModelId(result.pendingModelId);
1137
+ }
1138
+ if (result.exitRequested) {
1139
+ setTimeout(() => exit(), EXIT_DELAY_MS);
1140
+ }
1141
+ return result.handled;
907
1142
  },
908
1143
  [session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId]
909
1144
  );
910
1145
  }
911
- function StreamingIndicator({ text }) {
912
- if (text) {
913
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
914
- /* @__PURE__ */ jsxs7(Text9, { color: "cyan", bold: true, children: [
915
- "Robota:",
916
- " "
917
- ] }),
918
- /* @__PURE__ */ jsx9(Text9, { children: " " }),
919
- /* @__PURE__ */ jsx9(Box7, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { wrap: "wrap", children: renderMarkdown(text) }) })
920
- ] });
921
- }
922
- return /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "Thinking..." });
1146
+ function syncContextState(session, setter) {
1147
+ const ctx = session.getContextState();
1148
+ setter({ percentage: ctx.usedPercentage, usedTokens: ctx.usedTokens, maxTokens: ctx.maxTokens });
923
1149
  }
924
- async function runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextPercentage) {
1150
+ async function runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextState) {
925
1151
  setIsThinking(true);
926
1152
  clearStreamingText();
927
1153
  const historyBefore = session.getHistory().length;
@@ -929,29 +1155,15 @@ async function runSessionPrompt(prompt, session, addMessage, clearStreamingText,
929
1155
  const response = await session.run(prompt);
930
1156
  clearStreamingText();
931
1157
  const history = session.getHistory();
932
- const toolLines = [];
933
- for (let i = historyBefore; i < history.length; i++) {
934
- const msg = history[i];
935
- if (msg.role === "assistant" && msg.toolCalls) {
936
- for (const tc of msg.toolCalls) {
937
- let value = "";
938
- try {
939
- const parsed = JSON.parse(tc.function.arguments);
940
- const firstVal = Object.values(parsed)[0];
941
- value = typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal);
942
- } catch {
943
- value = tc.function.arguments;
944
- }
945
- const truncated = value.length > 80 ? value.slice(0, 77) + "..." : value;
946
- toolLines.push(`${tc.function.name}(${truncated})`);
947
- }
948
- }
949
- }
1158
+ const toolLines = extractToolCalls(
1159
+ history,
1160
+ historyBefore
1161
+ );
950
1162
  if (toolLines.length > 0) {
951
1163
  addMessage({ role: "tool", content: toolLines.join("\n"), toolName: `${toolLines.length} tools` });
952
1164
  }
953
1165
  addMessage({ role: "assistant", content: response || "(empty response)" });
954
- setContextPercentage(session.getContextState().usedPercentage);
1166
+ syncContextState(session, setContextState);
955
1167
  } catch (err) {
956
1168
  clearStreamingText();
957
1169
  if (err instanceof DOMException && err.name === "AbortError") {
@@ -980,13 +1192,13 @@ Execute the "${cmd}" skill: ${userInstruction}`;
980
1192
  }
981
1193
  return `Use the "${cmd}" skill: ${userInstruction}`;
982
1194
  }
983
- function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamingText, setIsThinking, setContextPercentage, registry) {
1195
+ function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamingText, setIsThinking, setContextState, registry) {
984
1196
  return useCallback3(
985
1197
  async (input) => {
986
1198
  if (input.startsWith("/")) {
987
1199
  const handled = await handleSlashCommand(input);
988
1200
  if (handled) {
989
- setContextPercentage(session.getContextState().usedPercentage);
1201
+ syncContextState(session, setContextState);
990
1202
  return;
991
1203
  }
992
1204
  const prompt = buildSkillPrompt(input, registry);
@@ -997,7 +1209,7 @@ function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamin
997
1209
  addMessage,
998
1210
  clearStreamingText,
999
1211
  setIsThinking,
1000
- setContextPercentage
1212
+ setContextState
1001
1213
  );
1002
1214
  }
1003
1215
  addMessage({ role: "user", content: input });
@@ -1007,7 +1219,7 @@ function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamin
1007
1219
  addMessage,
1008
1220
  clearStreamingText,
1009
1221
  setIsThinking,
1010
- setContextPercentage
1222
+ setContextState
1011
1223
  );
1012
1224
  },
1013
1225
  [
@@ -1016,7 +1228,7 @@ function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamin
1016
1228
  handleSlashCommand,
1017
1229
  clearStreamingText,
1018
1230
  setIsThinking,
1019
- setContextPercentage,
1231
+ setContextState,
1020
1232
  registry
1021
1233
  ]
1022
1234
  );
@@ -1033,10 +1245,10 @@ function useCommandRegistry(cwd) {
1033
1245
  }
1034
1246
  function App(props) {
1035
1247
  const { exit } = useApp();
1036
- const { session, permissionRequest, streamingText, clearStreamingText } = useSession(props);
1248
+ const { session, permissionRequest, streamingText, clearStreamingText, activeTools } = useSession(props);
1037
1249
  const { messages, setMessages, addMessage } = useMessages();
1038
1250
  const [isThinking, setIsThinking] = useState5(false);
1039
- const [contextPercentage, setContextPercentage] = useState5(0);
1251
+ const [contextState, setContextState] = useState5({ percentage: 0, usedTokens: 0, maxTokens: 0 });
1040
1252
  const registry = useCommandRegistry(props.cwd ?? process.cwd());
1041
1253
  const pendingModelChangeRef = useRef3(null);
1042
1254
  const [pendingModelId, setPendingModelId] = useState5(null);
@@ -1047,7 +1259,7 @@ function App(props) {
1047
1259
  handleSlashCommand,
1048
1260
  clearStreamingText,
1049
1261
  setIsThinking,
1050
- setContextPercentage,
1262
+ setContextState,
1051
1263
  registry
1052
1264
  );
1053
1265
  useInput5(
@@ -1057,26 +1269,26 @@ function App(props) {
1057
1269
  },
1058
1270
  { isActive: !permissionRequest }
1059
1271
  );
1060
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1061
- /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1062
- /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: `
1272
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1273
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1274
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: `
1063
1275
  ____ ___ ____ ___ _____ _
1064
1276
  | _ \\ / _ \\| __ ) / _ \\_ _|/ \\
1065
1277
  | |_) | | | | _ \\| | | || | / _ \\
1066
1278
  | _ <| |_| | |_) | |_| || |/ ___ \\
1067
1279
  |_| \\_\\\\___/|____/ \\___/ |_/_/ \\_\\
1068
1280
  ` }),
1069
- /* @__PURE__ */ jsxs7(Text9, { dimColor: true, children: [
1281
+ /* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
1070
1282
  " v",
1071
1283
  props.version ?? "0.0.0"
1072
1284
  ] })
1073
1285
  ] }),
1074
- /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
1075
- /* @__PURE__ */ jsx9(MessageList, { messages }),
1076
- isThinking && /* @__PURE__ */ jsx9(Box7, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx9(StreamingIndicator, { text: streamingText }) })
1286
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
1287
+ /* @__PURE__ */ jsx10(MessageList, { messages }),
1288
+ isThinking && /* @__PURE__ */ jsx10(Box8, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx10(StreamingIndicator, { text: streamingText, activeTools }) })
1077
1289
  ] }),
1078
- permissionRequest && /* @__PURE__ */ jsx9(PermissionPrompt, { request: permissionRequest }),
1079
- pendingModelId && /* @__PURE__ */ jsx9(
1290
+ permissionRequest && /* @__PURE__ */ jsx10(PermissionPrompt, { request: permissionRequest }),
1291
+ pendingModelId && /* @__PURE__ */ jsx10(
1080
1292
  ConfirmPrompt,
1081
1293
  {
1082
1294
  message: `Change model to ${getModelName(pendingModelId)}? This will restart the session.`,
@@ -1085,17 +1297,8 @@ function App(props) {
1085
1297
  pendingModelChangeRef.current = null;
1086
1298
  if (index === 0) {
1087
1299
  try {
1088
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
1089
- const settingsPath = join2(home, ".robota", "settings.json");
1090
- let settings = {};
1091
- if (existsSync2(settingsPath)) {
1092
- settings = JSON.parse(readFileSync2(settingsPath, "utf8"));
1093
- }
1094
- const provider = settings.provider ?? {};
1095
- provider.model = pendingModelId;
1096
- settings.provider = provider;
1097
- mkdirSync(join2(home, ".robota"), { recursive: true });
1098
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1300
+ const settingsPath = getUserSettingsPath();
1301
+ updateModelInSettings(settingsPath, pendingModelId);
1099
1302
  addMessage({ role: "system", content: `Model changed to ${getModelName(pendingModelId)}. Restarting...` });
1100
1303
  setTimeout(() => exit(), 500);
1101
1304
  } catch (err) {
@@ -1107,7 +1310,7 @@ function App(props) {
1107
1310
  }
1108
1311
  }
1109
1312
  ),
1110
- /* @__PURE__ */ jsx9(
1313
+ /* @__PURE__ */ jsx10(
1111
1314
  StatusBar,
1112
1315
  {
1113
1316
  permissionMode: session.getPermissionMode(),
@@ -1115,12 +1318,12 @@ function App(props) {
1115
1318
  sessionId: session.getSessionId(),
1116
1319
  messageCount: messages.length,
1117
1320
  isThinking,
1118
- contextPercentage,
1119
- contextUsedTokens: session.getContextState().usedTokens,
1120
- contextMaxTokens: session.getContextState().maxTokens
1321
+ contextPercentage: contextState.percentage,
1322
+ contextUsedTokens: contextState.usedTokens,
1323
+ contextMaxTokens: contextState.maxTokens
1121
1324
  }
1122
1325
  ),
1123
- /* @__PURE__ */ jsx9(
1326
+ /* @__PURE__ */ jsx10(
1124
1327
  InputArea,
1125
1328
  {
1126
1329
  onSubmit: handleSubmit,
@@ -1128,12 +1331,12 @@ function App(props) {
1128
1331
  registry
1129
1332
  }
1130
1333
  ),
1131
- /* @__PURE__ */ jsx9(Text9, { children: " " })
1334
+ /* @__PURE__ */ jsx10(Text10, { children: " " })
1132
1335
  ] });
1133
1336
  }
1134
1337
 
1135
1338
  // src/ui/render.tsx
1136
- import { jsx as jsx10 } from "react/jsx-runtime";
1339
+ import { jsx as jsx11 } from "react/jsx-runtime";
1137
1340
  function renderApp(options) {
1138
1341
  process.on("unhandledRejection", (reason) => {
1139
1342
  process.stderr.write(`
@@ -1144,7 +1347,7 @@ function renderApp(options) {
1144
1347
  `);
1145
1348
  }
1146
1349
  });
1147
- const instance = render(/* @__PURE__ */ jsx10(App, { ...options }), {
1350
+ const instance = render(/* @__PURE__ */ jsx11(App, { ...options }), {
1148
1351
  exitOnCtrlC: true
1149
1352
  });
1150
1353
  instance.waitUntilExit().catch((err) => {
@@ -1157,11 +1360,22 @@ function renderApp(options) {
1157
1360
  }
1158
1361
 
1159
1362
  // src/cli.ts
1160
- var VALID_MODES = ["plan", "default", "acceptEdits", "bypassPermissions"];
1363
+ function hasValidSettingsFile(filePath) {
1364
+ if (!existsSync3(filePath)) return false;
1365
+ try {
1366
+ const raw = readFileSync3(filePath, "utf8").trim();
1367
+ if (raw.length === 0) return false;
1368
+ const parsed = JSON.parse(raw);
1369
+ const provider = parsed.provider;
1370
+ return !!provider?.apiKey;
1371
+ } catch {
1372
+ return false;
1373
+ }
1374
+ }
1161
1375
  function readVersion() {
1162
1376
  try {
1163
1377
  const thisFile = fileURLToPath(import.meta.url);
1164
- const dir = dirname(thisFile);
1378
+ const dir = dirname2(thisFile);
1165
1379
  const candidates = [join3(dir, "..", "..", "package.json"), join3(dir, "..", "package.json")];
1166
1380
  for (const pkgPath of candidates) {
1167
1381
  try {
@@ -1178,108 +1392,11 @@ function readVersion() {
1178
1392
  return "0.0.0";
1179
1393
  }
1180
1394
  }
1181
- function parsePermissionMode(raw) {
1182
- if (raw === void 0) return void 0;
1183
- if (!VALID_MODES.includes(raw)) {
1184
- process.stderr.write(`Invalid --permission-mode "${raw}". Valid: ${VALID_MODES.join(" | ")}
1185
- `);
1186
- process.exit(1);
1187
- }
1188
- return raw;
1189
- }
1190
- function parseMaxTurns(raw) {
1191
- if (raw === void 0) return void 0;
1192
- const n = parseInt(raw, 10);
1193
- if (isNaN(n) || n <= 0) {
1194
- process.stderr.write(`Invalid --max-turns "${raw}". Must be a positive integer.
1195
- `);
1196
- process.exit(1);
1197
- }
1198
- return n;
1199
- }
1200
- function parseCliArgs() {
1201
- const { values, positionals } = parseArgs({
1202
- allowPositionals: true,
1203
- options: {
1204
- p: { type: "boolean", short: "p", default: false },
1205
- c: { type: "boolean", short: "c", default: false },
1206
- r: { type: "string", short: "r" },
1207
- model: { type: "string" },
1208
- "permission-mode": { type: "string" },
1209
- "max-turns": { type: "string" },
1210
- version: { type: "boolean", default: false },
1211
- reset: { type: "boolean", default: false }
1212
- }
1213
- });
1214
- return {
1215
- positional: positionals,
1216
- printMode: values["p"] ?? false,
1217
- continueMode: values["c"] ?? false,
1218
- resumeId: values["r"],
1219
- model: values["model"],
1220
- permissionMode: parsePermissionMode(values["permission-mode"]),
1221
- maxTurns: parseMaxTurns(values["max-turns"]),
1222
- version: values["version"] ?? false,
1223
- reset: values["reset"] ?? false
1224
- };
1225
- }
1226
- var PrintTerminal = class {
1227
- write(text) {
1228
- process.stdout.write(text);
1229
- }
1230
- writeLine(text) {
1231
- process.stdout.write(text + "\n");
1232
- }
1233
- writeMarkdown(md) {
1234
- process.stdout.write(md);
1235
- }
1236
- writeError(text) {
1237
- process.stderr.write(text + "\n");
1238
- }
1239
- prompt(question) {
1240
- return new Promise((resolve) => {
1241
- const rl = readline.createInterface({
1242
- input: process.stdin,
1243
- output: process.stdout,
1244
- terminal: false,
1245
- historySize: 0
1246
- });
1247
- rl.question(question, (answer) => {
1248
- rl.close();
1249
- resolve(answer);
1250
- });
1251
- });
1252
- }
1253
- async select(options, initialIndex = 0) {
1254
- for (let i = 0; i < options.length; i++) {
1255
- const marker = i === initialIndex ? ">" : " ";
1256
- process.stdout.write(` ${marker} ${i + 1}) ${options[i]}
1257
- `);
1258
- }
1259
- const answer = await this.prompt(
1260
- ` Choose [1-${options.length}] (default: ${options[initialIndex]}): `
1261
- );
1262
- const trimmed = answer.trim().toLowerCase();
1263
- if (trimmed === "") return initialIndex;
1264
- const num = parseInt(trimmed, 10);
1265
- if (!isNaN(num) && num >= 1 && num <= options.length) return num - 1;
1266
- return initialIndex;
1267
- }
1268
- spinner(_message) {
1269
- return { stop() {
1270
- }, update() {
1271
- } };
1272
- }
1273
- };
1274
- function getUserSettingsPath() {
1275
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
1276
- return join3(home, ".robota", "settings.json");
1277
- }
1278
1395
  async function ensureConfig(cwd) {
1279
1396
  const userPath = getUserSettingsPath();
1280
1397
  const projectPath = join3(cwd, ".robota", "settings.json");
1281
1398
  const localPath = join3(cwd, ".robota", "settings.local.json");
1282
- if (existsSync3(userPath) || existsSync3(projectPath) || existsSync3(localPath)) {
1399
+ if (hasValidSettingsFile(userPath) || hasValidSettingsFile(projectPath) || hasValidSettingsFile(localPath)) {
1283
1400
  return;
1284
1401
  }
1285
1402
  process.stdout.write("\n");
@@ -1323,7 +1440,7 @@ async function ensureConfig(cwd) {
1323
1440
  process.stderr.write("\n No API key provided. Exiting.\n");
1324
1441
  process.exit(1);
1325
1442
  }
1326
- const settingsDir = dirname(userPath);
1443
+ const settingsDir = dirname2(userPath);
1327
1444
  mkdirSync2(settingsDir, { recursive: true });
1328
1445
  const settings = {
1329
1446
  provider: {
@@ -1340,8 +1457,7 @@ async function ensureConfig(cwd) {
1340
1457
  }
1341
1458
  function resetConfig() {
1342
1459
  const userPath = getUserSettingsPath();
1343
- if (existsSync3(userPath)) {
1344
- unlinkSync2(userPath);
1460
+ if (deleteSettings(userPath)) {
1345
1461
  process.stdout.write(`Deleted ${userPath}
1346
1462
  `);
1347
1463
  } else {