@robota-sdk/agent-cli 3.0.0-beta.1 → 3.0.0-beta.10

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,6 +1,6 @@
1
1
  // src/cli.ts
2
2
  import { parseArgs } from "util";
3
- import { readFileSync as readFileSync2 } from "fs";
3
+ import { readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync, writeFileSync, unlinkSync } from "fs";
4
4
  import { join as join2, dirname } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import * as readline from "readline";
@@ -8,9 +8,10 @@ import {
8
8
  loadConfig,
9
9
  loadContext,
10
10
  detectProject,
11
- Session as Session2,
11
+ createSession as createSession2,
12
12
  SessionStore,
13
- buildSystemPrompt
13
+ FileSessionLogger as FileSessionLogger2,
14
+ projectPaths as projectPaths2
14
15
  } from "@robota-sdk/agent-sdk";
15
16
 
16
17
  // src/permissions/permission-prompt.ts
@@ -39,7 +40,7 @@ import { render } from "ink";
39
40
  // src/ui/App.tsx
40
41
  import { useState as useState4, useCallback as useCallback2, useRef as useRef2 } from "react";
41
42
  import { Box as Box6, Text as Text8, useApp, useInput as useInput4 } from "ink";
42
- import { Session } from "@robota-sdk/agent-sdk";
43
+ import { createSession, FileSessionLogger, projectPaths } from "@robota-sdk/agent-sdk";
43
44
 
44
45
  // src/commands/command-registry.ts
45
46
  var CommandRegistry = class {
@@ -101,6 +102,7 @@ function createBuiltinCommands() {
101
102
  { name: "cost", description: "Show session info", source: "builtin" },
102
103
  { name: "context", description: "Context window info", source: "builtin" },
103
104
  { name: "permissions", description: "Permission rules", source: "builtin" },
105
+ { name: "reset", description: "Delete settings and exit", source: "builtin" },
104
106
  { name: "exit", description: "Exit CLI", source: "builtin" }
105
107
  ];
106
108
  }
@@ -152,7 +154,8 @@ function scanSkillsDir(skillsDir) {
152
154
  commands.push({
153
155
  name: frontmatter?.name ?? entry.name,
154
156
  description: frontmatter?.description ?? `Skill: ${entry.name}`,
155
- source: "skill"
157
+ source: "skill",
158
+ skillContent: content
156
159
  });
157
160
  }
158
161
  return commands;
@@ -328,50 +331,60 @@ function CjkTextInput({
328
331
  }
329
332
  useInput(
330
333
  (input, key) => {
331
- if (key.upArrow || key.downArrow || key.ctrl && input === "c" || key.tab || key.shift && key.tab) {
332
- return;
333
- }
334
- if (key.return) {
335
- onSubmit?.(valueRef.current);
336
- return;
337
- }
338
- if (key.leftArrow) {
339
- if (cursorRef.current > 0) {
340
- cursorRef.current -= 1;
341
- forceRender((n) => n + 1);
334
+ try {
335
+ if (key.upArrow || key.downArrow || key.ctrl && input === "c" || key.tab || key.shift && key.tab) {
336
+ return;
342
337
  }
343
- return;
344
- }
345
- if (key.rightArrow) {
346
- if (cursorRef.current < valueRef.current.length) {
347
- cursorRef.current += 1;
348
- forceRender((n) => n + 1);
338
+ if (key.return) {
339
+ onSubmit?.(valueRef.current);
340
+ return;
349
341
  }
350
- return;
351
- }
352
- if (key.backspace || key.delete) {
353
- if (cursorRef.current > 0) {
354
- const v2 = valueRef.current;
355
- const next2 = v2.slice(0, cursorRef.current - 1) + v2.slice(cursorRef.current);
356
- cursorRef.current -= 1;
357
- valueRef.current = next2;
358
- onChange(next2);
342
+ if (key.leftArrow) {
343
+ if (cursorRef.current > 0) {
344
+ cursorRef.current -= 1;
345
+ forceRender((n) => n + 1);
346
+ }
347
+ return;
359
348
  }
360
- return;
349
+ if (key.rightArrow) {
350
+ if (cursorRef.current < valueRef.current.length) {
351
+ cursorRef.current += 1;
352
+ forceRender((n) => n + 1);
353
+ }
354
+ return;
355
+ }
356
+ if (key.backspace || key.delete) {
357
+ if (cursorRef.current > 0) {
358
+ const v2 = valueRef.current;
359
+ const next2 = v2.slice(0, cursorRef.current - 1) + v2.slice(cursorRef.current);
360
+ cursorRef.current -= 1;
361
+ valueRef.current = next2;
362
+ onChange(next2);
363
+ }
364
+ return;
365
+ }
366
+ if (!input || input.length === 0) return;
367
+ const printable = input.replace(/[\x00-\x1f\x7f]/g, "");
368
+ if (printable.length === 0) return;
369
+ const v = valueRef.current;
370
+ const c = cursorRef.current;
371
+ const next = v.slice(0, c) + printable + v.slice(c);
372
+ cursorRef.current = c + printable.length;
373
+ valueRef.current = next;
374
+ onChange(next);
375
+ } catch {
361
376
  }
362
- const v = valueRef.current;
363
- const c = cursorRef.current;
364
- const next = v.slice(0, c) + input + v.slice(c);
365
- cursorRef.current = c + input.length;
366
- valueRef.current = next;
367
- onChange(next);
368
377
  },
369
378
  { isActive: focus }
370
379
  );
371
380
  if (showCursor && focus) {
372
- const textBeforeCursor = [...valueRef.current].slice(0, cursorRef.current).join("");
373
- const cursorX = 4 + stringWidth(textBeforeCursor);
374
- setCursorPosition({ x: cursorX, y: 0 });
381
+ try {
382
+ const textBeforeCursor = [...valueRef.current].slice(0, cursorRef.current).join("");
383
+ const cursorX = 4 + stringWidth(textBeforeCursor);
384
+ setCursorPosition({ x: cursorX, y: 0 });
385
+ } catch {
386
+ setCursorPosition({ x: 4, y: 0 });
387
+ }
375
388
  }
376
389
  return /* @__PURE__ */ jsx3(Text3, { children: renderWithCursor(valueRef.current, cursorRef.current, placeholder, showCursor && focus) });
377
390
  }
@@ -607,7 +620,7 @@ function InputArea({ onSubmit, isDisabled, registry }) {
607
620
  import React4 from "react";
608
621
  import { Box as Box5, Text as Text7, useInput as useInput3 } from "ink";
609
622
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
610
- var OPTIONS = ["Allow", "Deny"];
623
+ var OPTIONS = ["Allow", "Allow always (this session)", "Deny"];
611
624
  function formatArgs2(args) {
612
625
  const entries = Object.entries(args);
613
626
  if (entries.length === 0) return "(no arguments)";
@@ -623,10 +636,12 @@ function PermissionPrompt({ request }) {
623
636
  setSelected(0);
624
637
  }
625
638
  const doResolve = React4.useCallback(
626
- (allowed) => {
639
+ (index) => {
627
640
  if (resolvedRef.current) return;
628
641
  resolvedRef.current = true;
629
- request.resolve(allowed);
642
+ if (index === 0) request.resolve(true);
643
+ else if (index === 1) request.resolve("allow-session");
644
+ else request.resolve(false);
630
645
  },
631
646
  [request]
632
647
  );
@@ -637,11 +652,13 @@ function PermissionPrompt({ request }) {
637
652
  } else if (key.downArrow || key.rightArrow) {
638
653
  setSelected((prev) => prev < OPTIONS.length - 1 ? prev + 1 : prev);
639
654
  } else if (key.return) {
640
- doResolve(selected === 0);
641
- } else if (input === "y" || input === "a" || input === "1") {
642
- doResolve(true);
643
- } else if (input === "n" || input === "d" || input === "2") {
644
- doResolve(false);
655
+ doResolve(selected);
656
+ } else if (input === "y" || input === "1") {
657
+ doResolve(0);
658
+ } else if (input === "a" || input === "2") {
659
+ doResolve(1);
660
+ } else if (input === "n" || input === "d" || input === "3") {
661
+ doResolve(2);
645
662
  }
646
663
  });
647
664
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
@@ -701,11 +718,11 @@ function useSession(props) {
701
718
  setPermissionRequest({
702
719
  toolName: next.toolName,
703
720
  toolArgs: next.toolArgs,
704
- resolve: (allowed) => {
721
+ resolve: (result) => {
705
722
  permissionQueueRef.current.shift();
706
723
  processingRef.current = false;
707
724
  setPermissionRequest(null);
708
- next.resolve(allowed);
725
+ next.resolve(result);
709
726
  setTimeout(() => processNextPermission(), 0);
710
727
  }
711
728
  });
@@ -721,10 +738,12 @@ function useSession(props) {
721
738
  const onTextDelta = (delta) => {
722
739
  setStreamingText((prev) => prev + delta);
723
740
  };
724
- sessionRef.current = new Session({
741
+ const paths = projectPaths(props.cwd ?? process.cwd());
742
+ sessionRef.current = createSession({
725
743
  config: props.config,
726
744
  context: props.context,
727
745
  terminal: NOOP_TERMINAL,
746
+ sessionLogger: new FileSessionLogger(paths.logs),
728
747
  projectInfo: props.projectInfo,
729
748
  sessionStore: props.sessionStore,
730
749
  permissionMode: props.permissionMode,
@@ -750,6 +769,7 @@ var HELP_TEXT = [
750
769
  " /compact [instr] \u2014 Compact context (optional focus instructions)",
751
770
  " /mode [m] \u2014 Show/change permission mode",
752
771
  " /cost \u2014 Show session info",
772
+ " /reset \u2014 Delete settings and exit",
753
773
  " /exit \u2014 Exit CLI"
754
774
  ].join("\n");
755
775
  function handleModeCommand(arg, session, addMessage) {
@@ -795,6 +815,39 @@ async function executeSlashCommand(cmd, parts, session, addMessage, setMessages,
795
815
  Messages: ${session.getMessageCount()}`
796
816
  });
797
817
  return true;
818
+ case "permissions": {
819
+ const mode = session.getPermissionMode();
820
+ const sessionAllowed = session.getSessionAllowedTools();
821
+ const lines = [`Permission mode: ${mode}`];
822
+ if (sessionAllowed.length > 0) {
823
+ lines.push(`Session-approved tools: ${sessionAllowed.join(", ")}`);
824
+ } else {
825
+ lines.push("No session-approved tools.");
826
+ }
827
+ addMessage({ role: "system", content: lines.join("\n") });
828
+ return true;
829
+ }
830
+ case "context": {
831
+ const ctx = session.getContextState();
832
+ addMessage({
833
+ role: "system",
834
+ content: `Context: ${ctx.usedTokens.toLocaleString()} / ${ctx.maxTokens.toLocaleString()} tokens (${Math.round(ctx.usedPercentage)}%)`
835
+ });
836
+ return true;
837
+ }
838
+ case "reset": {
839
+ const { existsSync: exists, unlinkSync: unlink } = await import("fs");
840
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
841
+ const settingsPath = `${home}/.robota/settings.json`;
842
+ if (exists(settingsPath)) {
843
+ unlink(settingsPath);
844
+ addMessage({ role: "system", content: `Deleted ${settingsPath}. Exiting...` });
845
+ } else {
846
+ addMessage({ role: "system", content: "No user settings found." });
847
+ }
848
+ setTimeout(() => exit(), 500);
849
+ return true;
850
+ }
798
851
  case "exit":
799
852
  exit();
800
853
  return true;
@@ -835,15 +888,42 @@ function StreamingIndicator({ text }) {
835
888
  async function runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextPercentage) {
836
889
  setIsThinking(true);
837
890
  clearStreamingText();
891
+ const historyBefore = session.getHistory().length;
838
892
  try {
839
893
  const response = await session.run(prompt);
840
894
  clearStreamingText();
895
+ const history = session.getHistory();
896
+ const toolLines = [];
897
+ for (let i = historyBefore; i < history.length; i++) {
898
+ const msg = history[i];
899
+ if (msg.role === "assistant" && msg.toolCalls) {
900
+ for (const tc of msg.toolCalls) {
901
+ let value = "";
902
+ try {
903
+ const parsed = JSON.parse(tc.function.arguments);
904
+ const firstVal = Object.values(parsed)[0];
905
+ value = typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal);
906
+ } catch {
907
+ value = tc.function.arguments;
908
+ }
909
+ const truncated = value.length > 80 ? value.slice(0, 77) + "..." : value;
910
+ toolLines.push(`${tc.function.name}(${truncated})`);
911
+ }
912
+ }
913
+ }
914
+ if (toolLines.length > 0) {
915
+ addMessage({ role: "tool", content: toolLines.join("\n"), toolName: `${toolLines.length} tools` });
916
+ }
841
917
  addMessage({ role: "assistant", content: response || "(empty response)" });
842
918
  setContextPercentage(session.getContextState().usedPercentage);
843
919
  } catch (err) {
844
920
  clearStreamingText();
845
- const errMsg = err instanceof Error ? err.message : String(err);
846
- addMessage({ role: "system", content: `Error: ${errMsg}` });
921
+ if (err instanceof DOMException && err.name === "AbortError") {
922
+ addMessage({ role: "system", content: "Cancelled." });
923
+ } else {
924
+ const errMsg = err instanceof Error ? err.message : String(err);
925
+ addMessage({ role: "system", content: `Error: ${errMsg}` });
926
+ }
847
927
  } finally {
848
928
  setIsThinking(false);
849
929
  }
@@ -854,7 +934,15 @@ function buildSkillPrompt(input, registry) {
854
934
  const skillCmd = registry.getCommands().find((c) => c.name === cmd && c.source === "skill");
855
935
  if (!skillCmd) return null;
856
936
  const args = parts.slice(1).join(" ").trim();
857
- return args ? `Use the "${cmd}" skill: ${args}` : `Use the "${cmd}" skill: ${skillCmd.description}`;
937
+ const userInstruction = args || skillCmd.description;
938
+ if (skillCmd.skillContent) {
939
+ return `<skill name="${cmd}">
940
+ ${skillCmd.skillContent}
941
+ </skill>
942
+
943
+ Execute the "${cmd}" skill: ${userInstruction}`;
944
+ }
945
+ return `Use the "${cmd}" skill: ${userInstruction}`;
858
946
  }
859
947
  function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamingText, setIsThinking, setContextPercentage, registry) {
860
948
  return useCallback2(
@@ -927,8 +1015,9 @@ function App(props) {
927
1015
  useInput4(
928
1016
  (_input, key) => {
929
1017
  if (key.ctrl && _input === "c") exit();
1018
+ if (key.escape && isThinking) session.abort();
930
1019
  },
931
- { isActive: !permissionRequest && !isThinking }
1020
+ { isActive: !permissionRequest }
932
1021
  );
933
1022
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
934
1023
  /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
@@ -1048,7 +1137,8 @@ function parseCliArgs() {
1048
1137
  model: { type: "string" },
1049
1138
  "permission-mode": { type: "string" },
1050
1139
  "max-turns": { type: "string" },
1051
- version: { type: "boolean", default: false }
1140
+ version: { type: "boolean", default: false },
1141
+ reset: { type: "boolean", default: false }
1052
1142
  }
1053
1143
  });
1054
1144
  return {
@@ -1059,7 +1149,8 @@ function parseCliArgs() {
1059
1149
  model: values["model"],
1060
1150
  permissionMode: parsePermissionMode(values["permission-mode"]),
1061
1151
  maxTurns: parseMaxTurns(values["max-turns"]),
1062
- version: values["version"] ?? false
1152
+ version: values["version"] ?? false,
1153
+ reset: values["reset"] ?? false
1063
1154
  };
1064
1155
  }
1065
1156
  var PrintTerminal = class {
@@ -1110,6 +1201,83 @@ var PrintTerminal = class {
1110
1201
  } };
1111
1202
  }
1112
1203
  };
1204
+ function getUserSettingsPath() {
1205
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
1206
+ return join2(home, ".robota", "settings.json");
1207
+ }
1208
+ async function ensureConfig(cwd) {
1209
+ const userPath = getUserSettingsPath();
1210
+ const projectPath = join2(cwd, ".robota", "settings.json");
1211
+ const localPath = join2(cwd, ".robota", "settings.local.json");
1212
+ if (existsSync2(userPath) || existsSync2(projectPath) || existsSync2(localPath)) {
1213
+ return;
1214
+ }
1215
+ process.stdout.write("\n");
1216
+ process.stdout.write(" Welcome to Robota CLI!\n");
1217
+ process.stdout.write(" No configuration found. Let's set up your API key.\n");
1218
+ process.stdout.write("\n");
1219
+ const apiKey = await new Promise((resolve) => {
1220
+ process.stdout.write(" Anthropic API key: ");
1221
+ let input = "";
1222
+ const stdin = process.stdin;
1223
+ const wasRaw = stdin.isRaw;
1224
+ stdin.setRawMode(true);
1225
+ stdin.resume();
1226
+ stdin.setEncoding("utf8");
1227
+ const onData = (data) => {
1228
+ for (const ch of data) {
1229
+ if (ch === "\r" || ch === "\n") {
1230
+ stdin.removeListener("data", onData);
1231
+ stdin.setRawMode(wasRaw ?? false);
1232
+ stdin.pause();
1233
+ process.stdout.write("\n");
1234
+ resolve(input.trim());
1235
+ return;
1236
+ } else if (ch === "\x7F" || ch === "\b") {
1237
+ if (input.length > 0) {
1238
+ input = input.slice(0, -1);
1239
+ process.stdout.write("\b \b");
1240
+ }
1241
+ } else if (ch === "") {
1242
+ process.stdout.write("\n");
1243
+ process.exit(0);
1244
+ } else if (ch.charCodeAt(0) >= 32) {
1245
+ input += ch;
1246
+ process.stdout.write("*");
1247
+ }
1248
+ }
1249
+ };
1250
+ stdin.on("data", onData);
1251
+ });
1252
+ if (!apiKey) {
1253
+ process.stderr.write("\n No API key provided. Exiting.\n");
1254
+ process.exit(1);
1255
+ }
1256
+ const settingsDir = dirname(userPath);
1257
+ mkdirSync(settingsDir, { recursive: true });
1258
+ const settings = {
1259
+ provider: {
1260
+ name: "anthropic",
1261
+ model: "claude-sonnet-4-6",
1262
+ apiKey
1263
+ }
1264
+ };
1265
+ writeFileSync(userPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1266
+ process.stdout.write(`
1267
+ Config saved to ${userPath}
1268
+
1269
+ `);
1270
+ }
1271
+ function resetConfig() {
1272
+ const userPath = getUserSettingsPath();
1273
+ if (existsSync2(userPath)) {
1274
+ unlinkSync(userPath);
1275
+ process.stdout.write(`Deleted ${userPath}
1276
+ `);
1277
+ } else {
1278
+ process.stdout.write("No user settings found.\n");
1279
+ }
1280
+ }
1113
1281
  async function startCli() {
1114
1282
  const args = parseCliArgs();
1115
1283
  if (args.version) {
@@ -1117,7 +1285,12 @@ async function startCli() {
1117
1285
  `);
1118
1286
  return;
1119
1287
  }
1288
+ if (args.reset) {
1289
+ resetConfig();
1290
+ return;
1291
+ }
1120
1292
  const cwd = process.cwd();
1293
+ await ensureConfig(cwd);
1121
1294
  const [config, context, projectInfo] = await Promise.all([
1122
1295
  loadConfig(cwd),
1123
1296
  loadContext(cwd),
@@ -1134,13 +1307,14 @@ async function startCli() {
1134
1307
  process.exit(1);
1135
1308
  }
1136
1309
  const terminal = new PrintTerminal();
1137
- const session = new Session2({
1310
+ const paths = projectPaths2(cwd);
1311
+ const session = createSession2({
1138
1312
  config,
1139
1313
  context,
1140
1314
  terminal,
1315
+ sessionLogger: new FileSessionLogger2(paths.logs),
1141
1316
  projectInfo,
1142
1317
  permissionMode: args.permissionMode,
1143
- systemPromptBuilder: buildSystemPrompt,
1144
1318
  promptForApproval
1145
1319
  });
1146
1320
  const response = await session.run(prompt);