@robinpath/cli 1.81.0 → 1.83.0

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.
Files changed (2) hide show
  1. package/dist/cli.mjs +310 -159
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -18598,7 +18598,7 @@ function getNativeModules() {
18598
18598
  import { join as join3, basename as basename2 } from "node:path";
18599
18599
  import { homedir as homedir2, platform as platform2 } from "node:os";
18600
18600
  import { existsSync as existsSync2 } from "node:fs";
18601
- var CLI_VERSION = true ? "1.81.0" : "1.81.0";
18601
+ var CLI_VERSION = true ? "1.83.0" : "1.83.0";
18602
18602
  var FLAG_QUIET = false;
18603
18603
  var FLAG_VERBOSE = false;
18604
18604
  var FLAG_AUTO_ACCEPT = false;
@@ -24156,33 +24156,137 @@ ${resultSummary}`
24156
24156
 
24157
24157
  // src/ink-repl.tsx
24158
24158
  import { useState, useCallback, useEffect, useMemo } from "react";
24159
- import { render, Box, Text, Static, useInput, useApp } from "ink";
24159
+ import { render, Box as Box2, Text as Text2, Static, useInput, useApp } from "ink";
24160
24160
  import InkSpinner from "ink-spinner";
24161
+
24162
+ // src/ui/Markdown.tsx
24163
+ import { Box, Text } from "ink";
24164
+ import { jsx, jsxs } from "react/jsx-runtime";
24165
+ function parseBlocks(text) {
24166
+ const blocks = [];
24167
+ const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
24168
+ let lastIndex = 0;
24169
+ let match;
24170
+ while ((match = codeBlockRegex.exec(text)) !== null) {
24171
+ if (match.index > lastIndex) {
24172
+ blocks.push({ type: "text", content: text.slice(lastIndex, match.index) });
24173
+ }
24174
+ blocks.push({ type: "code", content: match[2].trimEnd(), lang: match[1] || void 0 });
24175
+ lastIndex = match.index + match[0].length;
24176
+ }
24177
+ if (lastIndex < text.length) {
24178
+ blocks.push({ type: "text", content: text.slice(lastIndex) });
24179
+ }
24180
+ return blocks;
24181
+ }
24182
+ function renderInlineMarkdown(line) {
24183
+ const parts = [];
24184
+ let remaining = line;
24185
+ let key = 0;
24186
+ while (remaining.length > 0) {
24187
+ const boldMatch = remaining.match(/^\*\*(.*?)\*\*/);
24188
+ if (boldMatch) {
24189
+ parts.push(/* @__PURE__ */ jsx(Text, { bold: true, children: boldMatch[1] }, key++));
24190
+ remaining = remaining.slice(boldMatch[0].length);
24191
+ continue;
24192
+ }
24193
+ const codeMatch = remaining.match(/^`([^`]+)`/);
24194
+ if (codeMatch) {
24195
+ parts.push(/* @__PURE__ */ jsx(Text, { color: "yellow", children: codeMatch[1] }, key++));
24196
+ remaining = remaining.slice(codeMatch[0].length);
24197
+ continue;
24198
+ }
24199
+ const nextSpecial = remaining.search(/\*\*|`/);
24200
+ if (nextSpecial === -1) {
24201
+ parts.push(/* @__PURE__ */ jsx(Text, { children: remaining }, key++));
24202
+ break;
24203
+ }
24204
+ parts.push(/* @__PURE__ */ jsx(Text, { children: remaining.slice(0, nextSpecial) }, key++));
24205
+ remaining = remaining.slice(nextSpecial);
24206
+ }
24207
+ return parts;
24208
+ }
24209
+ function TextBlock({ content }) {
24210
+ const lines = content.split("\n");
24211
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: lines.map((line, i) => {
24212
+ const trimmed = line.trimStart();
24213
+ if (!trimmed) return /* @__PURE__ */ jsx(Text, { children: " " }, i);
24214
+ if (trimmed.startsWith("## ")) {
24215
+ return /* @__PURE__ */ jsx(Text, { bold: true, children: trimmed.slice(3) }, i);
24216
+ }
24217
+ if (trimmed.startsWith("# ")) {
24218
+ return /* @__PURE__ */ jsx(Text, { bold: true, children: trimmed.slice(2) }, i);
24219
+ }
24220
+ if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
24221
+ return /* @__PURE__ */ jsxs(Text, { children: [
24222
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " \u2022 " }),
24223
+ renderInlineMarkdown(trimmed.slice(2))
24224
+ ] }, i);
24225
+ }
24226
+ const numMatch = trimmed.match(/^(\d+)\.\s/);
24227
+ if (numMatch) {
24228
+ return /* @__PURE__ */ jsxs(Text, { children: [
24229
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
24230
+ " ",
24231
+ numMatch[1],
24232
+ ". "
24233
+ ] }),
24234
+ renderInlineMarkdown(trimmed.slice(numMatch[0].length))
24235
+ ] }, i);
24236
+ }
24237
+ return /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: renderInlineMarkdown(line) }, i);
24238
+ }) });
24239
+ }
24240
+ function CodeBlock({ content, lang }) {
24241
+ const w = Math.min(process.stdout.columns - 6 || 72, 72);
24242
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, marginX: 1, children: [
24243
+ /* @__PURE__ */ jsxs(Text, { children: [
24244
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u250C" }),
24245
+ lang ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${lang} ` }) : null,
24246
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(Math.max(0, w - (lang ? lang.length + 5 : 3))) })
24247
+ ] }),
24248
+ content.split("\n").map((line, i) => /* @__PURE__ */ jsxs(Text, { children: [
24249
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
24250
+ /* @__PURE__ */ jsx(Text, { color: "white", children: line })
24251
+ ] }, i)),
24252
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2514" + "\u2500".repeat(w - 3) })
24253
+ ] });
24254
+ }
24255
+ function Markdown({ children }) {
24256
+ const blocks = parseBlocks(children);
24257
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: blocks.map((block, i) => block.type === "code" ? /* @__PURE__ */ jsx(CodeBlock, { content: block.content, lang: block.lang }, i) : /* @__PURE__ */ jsx(TextBlock, { content: block.content }, i)) });
24258
+ }
24259
+
24260
+ // src/ink-repl.tsx
24161
24261
  import { platform as platform7 } from "node:os";
24162
24262
  import { randomUUID as randomUUID4 } from "node:crypto";
24163
- import { jsx, jsxs } from "react/jsx-runtime";
24263
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
24164
24264
  var nextId = 0;
24165
24265
  var COMMANDS = {
24166
24266
  "/model": "Switch AI model",
24167
- "/auto": "Toggle auto-accept",
24267
+ "/auto": "Toggle auto-accept (commands)",
24168
24268
  "/clear": "Clear conversation",
24269
+ "/compact": "Trim to last 10 messages",
24169
24270
  "/save": "Save session",
24170
- "/sessions": "List sessions",
24171
- "/memory": "Show memory",
24271
+ "/sessions": "List saved sessions",
24272
+ "/resume": "Resume a session",
24273
+ "/delete": "Delete a session",
24274
+ "/memory": "Show persistent memory",
24172
24275
  "/remember": "Save a fact",
24276
+ "/forget": "Remove a memory",
24173
24277
  "/usage": "Token usage & cost",
24174
24278
  "/shell": "Switch shell",
24175
- "/help": "Show commands"
24279
+ "/help": "All commands"
24176
24280
  };
24177
24281
  function InputArea({ onSubmit, placeholder }) {
24178
24282
  const [value, setValue] = useState("");
24179
- const [showHints, setShowHints] = useState(false);
24180
24283
  const { exit } = useApp();
24181
24284
  const matchingCommands = useMemo(() => {
24182
24285
  if (!value.startsWith("/")) return [];
24183
24286
  if (value === "/") return Object.entries(COMMANDS);
24184
24287
  return Object.entries(COMMANDS).filter(([cmd]) => cmd.startsWith(value));
24185
24288
  }, [value]);
24289
+ const showHints = value.startsWith("/") && matchingCommands.length > 0;
24186
24290
  useInput((ch, key) => {
24187
24291
  if (key.return) {
24188
24292
  if (value.endsWith("\\")) {
@@ -24193,7 +24297,6 @@ function InputArea({ onSubmit, placeholder }) {
24193
24297
  if (text) {
24194
24298
  onSubmit(text);
24195
24299
  setValue("");
24196
- setShowHints(false);
24197
24300
  }
24198
24301
  return;
24199
24302
  }
@@ -24203,15 +24306,11 @@ function InputArea({ onSubmit, placeholder }) {
24203
24306
  }
24204
24307
  if (key.escape) {
24205
24308
  setValue("");
24206
- setShowHints(false);
24207
24309
  return;
24208
24310
  }
24209
24311
  if (ch === "") {
24210
24312
  if (!value) exit();
24211
- else {
24212
- setValue("");
24213
- setShowHints(false);
24214
- }
24313
+ else setValue("");
24215
24314
  return;
24216
24315
  }
24217
24316
  if (key.backspace || key.delete) {
@@ -24219,10 +24318,7 @@ function InputArea({ onSubmit, placeholder }) {
24219
24318
  return;
24220
24319
  }
24221
24320
  if (key.tab) {
24222
- if (matchingCommands.length === 1) {
24223
- setValue(matchingCommands[0][0] + " ");
24224
- setShowHints(false);
24225
- }
24321
+ if (matchingCommands.length === 1) setValue(matchingCommands[0][0]);
24226
24322
  return;
24227
24323
  }
24228
24324
  if (ch === "") {
@@ -24235,169 +24331,193 @@ function InputArea({ onSubmit, placeholder }) {
24235
24331
  }
24236
24332
  if (ch && !key.ctrl && !key.meta) setValue((p) => p + ch);
24237
24333
  });
24238
- useEffect(() => {
24239
- setShowHints(value.startsWith("/") && matchingCommands.length > 0);
24240
- }, [value, matchingCommands.length]);
24241
24334
  const lines = value.split("\n");
24242
24335
  const empty = value === "";
24243
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
24244
- showHints && /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginX: 2, marginBottom: 1, children: matchingCommands.slice(0, 6).map(([cmd, desc]) => /* @__PURE__ */ jsxs(Text, { children: [
24245
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: cmd.padEnd(12) }),
24246
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: desc })
24336
+ const w = Math.min(process.stdout.columns - 4 || 76, 76);
24337
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
24338
+ showHints && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginX: 2, marginBottom: 1, children: matchingCommands.slice(0, 8).map(([cmd, desc]) => /* @__PURE__ */ jsxs2(Text2, { children: [
24339
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: cmd.padEnd(14) }),
24340
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: desc })
24247
24341
  ] }, cmd)) }),
24248
- /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginX: 1, children: [
24249
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(Math.min(process.stdout.columns - 4, 76)) }),
24250
- /* @__PURE__ */ jsx(Box, { paddingX: 1, children: empty ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
24342
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginX: 1, children: [
24343
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500".repeat(w) }),
24344
+ /* @__PURE__ */ jsx2(Box2, { paddingX: 1, flexDirection: "column", children: empty ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
24251
24345
  "> ",
24252
24346
  placeholder
24253
- ] }) : /* @__PURE__ */ jsxs(Text, { children: [
24254
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: "> " }),
24255
- lines.map((line, i) => /* @__PURE__ */ jsxs(Text, { children: [
24256
- i > 0 ? "\n " : "",
24257
- line,
24258
- i === lines.length - 1 ? /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u258E" }) : null
24259
- ] }, i))
24260
- ] }) }),
24261
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(Math.min(process.stdout.columns - 4, 76)) })
24347
+ ] }) : lines.map((line, i) => /* @__PURE__ */ jsxs2(Text2, { children: [
24348
+ i === 0 ? /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "> " }) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }),
24349
+ line,
24350
+ i === lines.length - 1 ? /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u258E" }) : null
24351
+ ] }, i)) }),
24352
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500".repeat(w) })
24262
24353
  ] }),
24263
- /* @__PURE__ */ jsx(Box, { marginX: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
24264
- /* @__PURE__ */ jsx(Text, { color: "gray", children: "enter" }),
24354
+ /* @__PURE__ */ jsx2(Box2, { marginX: 2, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
24355
+ /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "enter" }),
24265
24356
  " send ",
24266
- /* @__PURE__ */ jsx(Text, { color: "gray", children: "\\" }),
24357
+ /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "\\" }),
24267
24358
  " newline ",
24268
- /* @__PURE__ */ jsx(Text, { color: "gray", children: "esc" }),
24269
- " clear ",
24270
- /* @__PURE__ */ jsx(Text, { color: "gray", children: "/" }),
24359
+ /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "/" }),
24271
24360
  " commands ",
24272
- /* @__PURE__ */ jsx(Text, { color: "gray", children: "tab" }),
24273
- " complete"
24361
+ /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "tab" }),
24362
+ " complete ",
24363
+ /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "@/" }),
24364
+ " files"
24274
24365
  ] }) })
24275
24366
  ] });
24276
24367
  }
24277
- function ChatApp({ onMessage, statusText }) {
24368
+ function ChatApp({ engine }) {
24278
24369
  const [messages, setMessages] = useState([]);
24279
24370
  const [streaming, setStreaming] = useState("");
24280
24371
  const [loading, setLoading] = useState(false);
24372
+ const [status, setStatus] = useState("");
24281
24373
  useEffect(() => {
24282
- global.__rpUI = {
24374
+ engine.ui = {
24283
24375
  setStreaming,
24284
24376
  setLoading,
24377
+ setStatus,
24285
24378
  addMessage: (text, dim) => setMessages((p) => [...p, { id: ++nextId, text, dim }])
24286
24379
  };
24380
+ engine.updateStatus();
24287
24381
  }, []);
24288
24382
  const handleSubmit = useCallback(async (text) => {
24289
24383
  if (text === "exit" || text === "quit") {
24290
- global.__rpExit?.();
24384
+ engine.exit();
24385
+ return;
24386
+ }
24387
+ if (text.startsWith("/")) {
24388
+ const result = await engine.handleSlashCommand(text);
24389
+ if (result) setMessages((p) => [...p, { id: ++nextId, text: result, dim: true }]);
24390
+ engine.updateStatus();
24291
24391
  return;
24292
24392
  }
24293
24393
  setMessages((p) => [...p, { id: ++nextId, text: `\u276F ${text}` }]);
24294
24394
  setLoading(true);
24295
24395
  setStreaming("");
24296
24396
  try {
24297
- const response = await onMessage(text);
24397
+ const response = await engine.handleAIMessage(text);
24298
24398
  if (response) setMessages((p) => [...p, { id: ++nextId, text: response }]);
24299
24399
  } catch (err) {
24300
24400
  setMessages((p) => [...p, { id: ++nextId, text: `Error: ${err.message}`, dim: true }]);
24301
24401
  }
24302
24402
  setLoading(false);
24303
24403
  setStreaming("");
24304
- }, [onMessage]);
24404
+ engine.updateStatus();
24405
+ }, [engine]);
24305
24406
  const isFirst = messages.length === 0;
24306
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
24307
- /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
24308
- /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u25C6" }),
24407
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", padding: 1, children: [
24408
+ /* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsxs2(Text2, { children: [
24409
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "\u25C6" }),
24309
24410
  " ",
24310
- /* @__PURE__ */ jsx(Text, { bold: true, children: "RobinPath" }),
24411
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: "RobinPath" }),
24311
24412
  " ",
24312
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
24413
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
24313
24414
  "v",
24314
24415
  CLI_VERSION
24315
24416
  ] })
24316
24417
  ] }) }),
24317
- /* @__PURE__ */ jsx(Static, { items: messages, children: (msg) => /* @__PURE__ */ jsx(Box, { paddingX: 1, marginBottom: msg.text.startsWith("\u276F") ? 0 : 1, children: msg.dim ? /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "wrap", children: msg.text }) : /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: msg.text }) }, msg.id) }),
24318
- loading ? /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, children: streaming ? /* @__PURE__ */ jsxs(Text, { wrap: "wrap", children: [
24319
- streaming,
24320
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u258D" })
24321
- ] }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
24322
- /* @__PURE__ */ jsx(InkSpinner, { type: "dots" }),
24418
+ /* @__PURE__ */ jsx2(Static, { items: messages, children: (msg) => /* @__PURE__ */ jsx2(Box2, { paddingX: 1, marginBottom: msg.text.startsWith("\u276F") ? 0 : 1, flexDirection: "column", children: msg.text.startsWith("\u276F") ? /* @__PURE__ */ jsxs2(Text2, { children: [
24419
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "\u276F" }),
24420
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: msg.text.slice(1) })
24421
+ ] }) : msg.dim ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "wrap", children: msg.text }) : /* @__PURE__ */ jsx2(Markdown, { children: msg.text }) }, msg.id) }),
24422
+ loading ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingX: 1, children: streaming ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
24423
+ /* @__PURE__ */ jsx2(Markdown, { children: streaming }),
24424
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u258D" })
24425
+ ] }) : /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
24426
+ /* @__PURE__ */ jsx2(InkSpinner, { type: "dots" }),
24323
24427
  " Thinking"
24324
- ] }) }) : /* @__PURE__ */ jsx(
24428
+ ] }) }) : /* @__PURE__ */ jsx2(
24325
24429
  InputArea,
24326
24430
  {
24327
24431
  onSubmit: handleSubmit,
24328
24432
  placeholder: isFirst ? "Anything to automate with RobinPath?" : "Ask anything..."
24329
24433
  }
24330
24434
  ),
24331
- /* @__PURE__ */ jsx(Box, { marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: statusText }) })
24435
+ status ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: status }) }) : null
24332
24436
  ] });
24333
24437
  }
24334
- async function startInkREPL(initialPrompt, resumeSessionId, opts = {}) {
24335
- const config = readAiConfig();
24336
- let autoAccept = opts.autoAccept || false;
24337
- const devMode = opts.devMode || false;
24338
- if (devMode) setFlags({ verbose: true });
24339
- const resolveProvider = (key) => {
24438
+ var ReplEngine = class {
24439
+ config;
24440
+ autoAccept;
24441
+ apiKey;
24442
+ model;
24443
+ sessionId;
24444
+ sessionName;
24445
+ usage;
24446
+ conversationMessages;
24447
+ cliContext;
24448
+ ui = null;
24449
+ constructor(resumeSessionId, opts) {
24450
+ this.config = readAiConfig();
24451
+ this.autoAccept = opts.autoAccept || false;
24452
+ if (opts.devMode) setFlags({ verbose: true });
24453
+ this.apiKey = this.config.apiKey || null;
24454
+ this.model = this.apiKey ? this.config.model || "anthropic/claude-sonnet-4.6" : "robinpath-default";
24455
+ this.sessionId = resumeSessionId || randomUUID4().slice(0, 8);
24456
+ this.sessionName = `session-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
24457
+ this.usage = createUsageTracker();
24458
+ this.conversationMessages = [];
24459
+ this.cliContext = {
24460
+ platform: platform7(),
24461
+ shell: getShellConfig().name,
24462
+ cwd: process.cwd(),
24463
+ cliVersion: CLI_VERSION,
24464
+ nativeModules: getNativeModules().map((m) => m.name),
24465
+ installedModules: Object.keys(readModulesManifest())
24466
+ };
24467
+ const mem = buildMemoryContext();
24468
+ if (mem.trim()) {
24469
+ this.conversationMessages.push({ role: "user", content: `[Context] ${mem.trim()}` });
24470
+ this.conversationMessages.push({ role: "assistant", content: "Preferences loaded." });
24471
+ }
24472
+ if (resumeSessionId) {
24473
+ const session = loadSession(resumeSessionId);
24474
+ if (session) {
24475
+ this.sessionName = session.name;
24476
+ for (const msg of session.messages) this.conversationMessages.push(msg);
24477
+ if (session.usage) Object.assign(this.usage, session.usage);
24478
+ }
24479
+ }
24480
+ }
24481
+ resolveProvider(key) {
24340
24482
  if (!key) return "gemini";
24341
24483
  if (key.startsWith("sk-or-")) return "openrouter";
24342
24484
  if (key.startsWith("sk-ant-")) return "anthropic";
24343
24485
  if (key.startsWith("sk-")) return "openai";
24344
- return config.provider || "gemini";
24345
- };
24346
- const apiKey = config.apiKey || null;
24347
- const model = apiKey ? config.model || "anthropic/claude-sonnet-4.6" : "robinpath-default";
24348
- const modelShort = (m) => m === "robinpath-default" ? "gemini-free" : m.includes("/") ? m.split("/").pop() : m;
24349
- const cliContext = {
24350
- platform: platform7(),
24351
- shell: getShellConfig().name,
24352
- cwd: process.cwd(),
24353
- cliVersion: CLI_VERSION,
24354
- nativeModules: getNativeModules().map((m) => m.name),
24355
- installedModules: Object.keys(readModulesManifest())
24356
- };
24357
- let sessionId = resumeSessionId || randomUUID4().slice(0, 8);
24358
- let sessionName = `session-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
24359
- const usage = createUsageTracker();
24360
- const conversationMessages = [];
24361
- const mem = buildMemoryContext();
24362
- if (mem.trim()) {
24363
- conversationMessages.push({ role: "user", content: `[Context] ${mem.trim()}` });
24364
- conversationMessages.push({ role: "assistant", content: "Preferences loaded." });
24486
+ return this.config.provider || "gemini";
24365
24487
  }
24366
- if (resumeSessionId) {
24367
- const session = loadSession(resumeSessionId);
24368
- if (session) {
24369
- sessionName = session.name;
24370
- for (const msg of session.messages) conversationMessages.push(msg);
24371
- if (session.usage) Object.assign(usage, session.usage);
24372
- }
24488
+ updateStatus() {
24489
+ const m = this.model.includes("/") ? this.model.split("/").pop() : this.model;
24490
+ const cost = this.usage.cost > 0 ? ` \xB7 $${this.usage.cost.toFixed(4)}` : "";
24491
+ const tokens = this.usage.totalTokens > 0 ? ` \xB7 ${this.usage.totalTokens.toLocaleString()} tok` : "";
24492
+ this.ui?.setStatus(`${m} \xB7 ${getShellConfig().name} \xB7 ${this.autoAccept ? "auto" : "confirm"}${tokens}${cost}`);
24373
24493
  }
24374
- function getStatusText() {
24375
- const m = modelShort(readAiConfig().model || model);
24376
- const cost = usage.cost > 0 ? ` \xB7 $${usage.cost.toFixed(4)}` : "";
24377
- const tokens = usage.totalTokens > 0 ? ` \xB7 ${usage.totalTokens.toLocaleString()} tok` : "";
24378
- return `${m} \xB7 ${getShellConfig().name} \xB7 ${autoAccept ? "auto" : "confirm"}${tokens}${cost}`;
24494
+ exit() {
24495
+ if (this.conversationMessages.length > 1) saveSession(this.sessionId, this.sessionName, this.conversationMessages, this.usage);
24496
+ process.exit(0);
24379
24497
  }
24380
- async function handleMessage(text) {
24381
- const ui = global.__rpUI;
24498
+ // ── Slash commands — return display text, not a chat message ──
24499
+ async handleSlashCommand(text) {
24382
24500
  if (text === "/" || text === "/help") {
24383
- return Object.entries(COMMANDS).map(([cmd, desc]) => `${cmd.padEnd(12)} ${desc}`).join("\n");
24501
+ return Object.entries(COMMANDS).map(([cmd, desc]) => `${cmd.padEnd(14)} ${desc}`).join("\n");
24384
24502
  }
24385
24503
  if (text === "/clear") {
24386
- conversationMessages.length = 0;
24387
- return "Conversation cleared.";
24504
+ this.conversationMessages.length = 0;
24505
+ return "\u2713 Conversation cleared.";
24388
24506
  }
24389
- if (text === "/usage") {
24390
- const c = usage.cost > 0 ? `$${usage.cost.toFixed(4)}` : "$0.00 (free)";
24391
- return `${usage.totalTokens.toLocaleString()} tokens \xB7 ${usage.requests} requests \xB7 ${c}`;
24507
+ if (text === "/compact") {
24508
+ if (this.conversationMessages.length > 12) {
24509
+ this.conversationMessages.splice(1, this.conversationMessages.length - 11);
24510
+ }
24511
+ return `\u2713 Trimmed to ${this.conversationMessages.length} messages.`;
24392
24512
  }
24393
24513
  if (text === "/auto") {
24394
- autoAccept = !autoAccept;
24395
- return `Auto-accept: ${autoAccept ? "ON" : "OFF"}`;
24514
+ this.autoAccept = !this.autoAccept;
24515
+ return `Auto-accept: ${this.autoAccept ? "ON \u2014 commands run without asking" : "OFF \u2014 confirm each command"}`;
24396
24516
  }
24397
24517
  if (text === "/model") {
24398
24518
  const hasKey = !!readAiConfig().apiKey;
24399
24519
  const models = hasKey ? AI_MODELS : AI_MODELS.filter((m) => !m.requiresKey);
24400
- const cur = readAiConfig().model || model;
24520
+ const cur = readAiConfig().model || this.model;
24401
24521
  return models.map((m, i) => {
24402
24522
  const mark = m.id === cur ? " \u2713" : "";
24403
24523
  return `${String(i + 1).padStart(2)}. ${m.name.padEnd(22)} ${m.desc}${mark}`;
@@ -24408,33 +24528,61 @@ async function startInkREPL(initialPrompt, resumeSessionId, opts = {}) {
24408
24528
  const models = hasKey ? AI_MODELS : AI_MODELS.filter((m) => !m.requiresKey);
24409
24529
  const idx = parseInt(text.split(" ")[1], 10) - 1;
24410
24530
  if (idx >= 0 && idx < models.length) {
24411
- config.model = models[idx].id;
24412
- writeAiConfig(config);
24413
- return `Model changed to ${models[idx].name}`;
24531
+ this.config.model = models[idx].id;
24532
+ this.model = models[idx].id;
24533
+ writeAiConfig(this.config);
24534
+ return `\u2713 Model: ${models[idx].name}`;
24414
24535
  }
24415
- return "Invalid number.";
24536
+ return "Invalid number. Type /model to see the list.";
24537
+ }
24538
+ if (text === "/usage") {
24539
+ const c = this.usage.cost > 0 ? `$${this.usage.cost.toFixed(4)}` : "$0.00 (free)";
24540
+ return `${this.usage.totalTokens.toLocaleString()} tokens \xB7 ${this.usage.requests} requests \xB7 ${c}`;
24416
24541
  }
24417
24542
  if (text === "/memory") {
24418
24543
  const m = loadMemory();
24419
- return m.facts.length ? m.facts.map((f, i) => `${i + 1}. ${f}`).join("\n") : "No memories saved.";
24544
+ return m.facts.length ? m.facts.map((f, i) => `${i + 1}. ${f}`).join("\n") : "No memories saved yet.\nUse /remember <fact> to save something.";
24420
24545
  }
24421
24546
  if (text.startsWith("/remember ")) {
24422
- addMemoryFact(text.slice(10).trim());
24423
- return "Remembered.";
24547
+ const fact = text.slice(10).trim();
24548
+ if (!fact) return "Usage: /remember <fact>";
24549
+ addMemoryFact(fact);
24550
+ return `\u2713 Remembered: "${fact}"`;
24424
24551
  }
24425
24552
  if (text.startsWith("/forget ")) {
24426
- removeMemoryFact(parseInt(text.slice(8).trim(), 10) - 1);
24427
- return "Forgotten.";
24553
+ const idx = parseInt(text.slice(8).trim(), 10) - 1;
24554
+ const removed = removeMemoryFact(idx);
24555
+ return removed ? `\u2713 Forgot: "${removed}"` : "Invalid number. Type /memory to see the list.";
24428
24556
  }
24429
24557
  if (text === "/save" || text.startsWith("/save ")) {
24430
- if (text.length > 5) sessionName = text.slice(5).trim();
24431
- saveSession(sessionId, sessionName, conversationMessages, usage);
24432
- return `Session saved: ${sessionName}`;
24558
+ if (text.length > 5) this.sessionName = text.slice(5).trim();
24559
+ saveSession(this.sessionId, this.sessionName, this.conversationMessages, this.usage);
24560
+ return `\u2713 Saved: ${this.sessionName} (${this.sessionId})`;
24433
24561
  }
24434
24562
  if (text === "/sessions") {
24435
24563
  const sessions = listSessions();
24436
24564
  if (sessions.length === 0) return "No saved sessions.";
24437
- return sessions.map((s) => `${s.id} ${s.name} (${s.messages} msgs)`).join("\n");
24565
+ return sessions.map((s) => {
24566
+ const age = Math.round((Date.now() - new Date(s.updated || s.created).getTime()) / 6e4);
24567
+ const ago = age < 60 ? `${age}m` : age < 1440 ? `${Math.floor(age / 60)}h` : `${Math.floor(age / 1440)}d`;
24568
+ return `${s.id} ${s.name.padEnd(20)} ${s.messages} msgs \xB7 ${ago} ago`;
24569
+ }).join("\n") + "\n\nType /resume <id> to resume.";
24570
+ }
24571
+ if (text.startsWith("/resume ")) {
24572
+ const targetId = text.slice(8).trim();
24573
+ const session = loadSession(targetId);
24574
+ if (!session) return `Session '${targetId}' not found.`;
24575
+ this.sessionId = session.id;
24576
+ this.sessionName = session.name;
24577
+ this.conversationMessages.length = 0;
24578
+ for (const msg of session.messages) this.conversationMessages.push(msg);
24579
+ if (session.usage) Object.assign(this.usage, session.usage);
24580
+ return `\u2713 Resumed: ${session.name} (${session.messages.length} msgs)`;
24581
+ }
24582
+ if (text.startsWith("/delete ")) {
24583
+ const targetId = text.slice(8).trim();
24584
+ const deleted = deleteSession(targetId);
24585
+ return deleted ? `\u2713 Deleted session ${targetId}` : `Session '${targetId}' not found.`;
24438
24586
  }
24439
24587
  if (text === "/shell") {
24440
24588
  return getAvailableShells().map((s) => {
@@ -24444,21 +24592,26 @@ async function startInkREPL(initialPrompt, resumeSessionId, opts = {}) {
24444
24592
  }
24445
24593
  if (text.startsWith("/shell ")) {
24446
24594
  setShellOverride(text.slice(7).trim());
24447
- cliContext.shell = getShellConfig().name;
24448
- return `Shell: ${getShellConfig().name}`;
24595
+ this.cliContext.shell = getShellConfig().name;
24596
+ return `\u2713 Shell: ${getShellConfig().name}`;
24449
24597
  }
24450
- if (text.startsWith("/")) return `Unknown command: ${text}. Type / for help.`;
24598
+ return `Unknown command: ${text}
24599
+ Type / to see available commands.`;
24600
+ }
24601
+ // ── AI message ──
24602
+ async handleAIMessage(text) {
24603
+ const ui = this.ui;
24451
24604
  const { expanded } = expandFileRefs(text);
24452
- conversationMessages.push({ role: "user", content: expanded });
24453
- await autoCompact(conversationMessages);
24454
- const activeModel = readAiConfig().model || model;
24455
- const activeKey = readAiConfig().apiKey || apiKey;
24456
- const activeProvider = resolveProvider(activeKey);
24605
+ this.conversationMessages.push({ role: "user", content: expanded });
24606
+ await autoCompact(this.conversationMessages);
24607
+ const activeModel = readAiConfig().model || this.model;
24608
+ const activeKey = readAiConfig().apiKey || this.apiKey;
24609
+ const activeProvider = this.resolveProvider(activeKey);
24457
24610
  let finalResponse = "";
24458
24611
  for (let loop = 0; loop < 15; loop++) {
24459
24612
  let fullText = "";
24460
24613
  const result = await fetchBrainStream(
24461
- loop === 0 ? expanded : conversationMessages[conversationMessages.length - 1].content,
24614
+ loop === 0 ? expanded : this.conversationMessages[this.conversationMessages.length - 1].content,
24462
24615
  {
24463
24616
  onToken: (delta) => {
24464
24617
  if (delta === "\x1B[RETRY]") {
@@ -24470,11 +24623,11 @@ async function startInkREPL(initialPrompt, resumeSessionId, opts = {}) {
24470
24623
  const clean = fullText.replace(/<memory>[\s\S]*?<\/memory>/g, "").replace(/<cmd>[\s\S]*?<\/cmd>/g, "").replace(/\n{3,}/g, "\n\n").trim();
24471
24624
  ui?.setStreaming(clean);
24472
24625
  },
24473
- conversationHistory: conversationMessages.slice(0, -1),
24626
+ conversationHistory: this.conversationMessages.slice(0, -1),
24474
24627
  provider: activeProvider,
24475
24628
  model: activeModel,
24476
24629
  apiKey: activeKey,
24477
- cliContext
24630
+ cliContext: this.cliContext
24478
24631
  }
24479
24632
  );
24480
24633
  if (!result) {
@@ -24492,15 +24645,15 @@ async function startInkREPL(initialPrompt, resumeSessionId, opts = {}) {
24492
24645
  if (result.usage) {
24493
24646
  const pt2 = result.usage.prompt_tokens || 0;
24494
24647
  const ct2 = result.usage.completion_tokens || 0;
24495
- usage.promptTokens += pt2;
24496
- usage.completionTokens += ct2;
24497
- usage.totalTokens += pt2 + ct2;
24498
- usage.requests++;
24499
- usage.cost += estimateCost(activeModel, pt2, ct2);
24648
+ this.usage.promptTokens += pt2;
24649
+ this.usage.completionTokens += ct2;
24650
+ this.usage.totalTokens += pt2 + ct2;
24651
+ this.usage.requests++;
24652
+ this.usage.cost += estimateCost(activeModel, pt2, ct2);
24500
24653
  }
24501
24654
  const { cleaned } = extractMemoryTags(stripCommandTags(result.code));
24502
24655
  const commands = extractCommands(result.code);
24503
- if (cleaned) conversationMessages.push({ role: "assistant", content: cleaned });
24656
+ if (cleaned) this.conversationMessages.push({ role: "assistant", content: cleaned });
24504
24657
  if (commands.length === 0) {
24505
24658
  finalResponse = cleaned || fullText;
24506
24659
  break;
@@ -24518,35 +24671,33 @@ async function startInkREPL(initialPrompt, resumeSessionId, opts = {}) {
24518
24671
  }
24519
24672
  const summary = commands.map((cmd) => `$ ${cmd}
24520
24673
  (executed)`).join("\n");
24521
- conversationMessages.push({ role: "user", content: `[Results]
24674
+ this.conversationMessages.push({ role: "user", content: `[Results]
24522
24675
  ${summary}` });
24523
24676
  ui?.setStreaming("");
24524
24677
  finalResponse = "";
24525
24678
  }
24526
- saveSession(sessionId, sessionName, conversationMessages, usage);
24679
+ saveSession(this.sessionId, this.sessionName, this.conversationMessages, this.usage);
24527
24680
  return finalResponse;
24528
24681
  }
24529
- const { waitUntilExit } = render(
24530
- /* @__PURE__ */ jsx(ChatApp, { onMessage: handleMessage, statusText: getStatusText() })
24531
- );
24532
- global.__rpExit = () => {
24533
- if (conversationMessages.length > 1) saveSession(sessionId, sessionName, conversationMessages, usage);
24534
- process.exit(0);
24535
- };
24682
+ };
24683
+ async function startInkREPL(initialPrompt, resumeSessionId, opts = {}) {
24684
+ const engine = new ReplEngine(resumeSessionId, opts);
24685
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx2(ChatApp, { engine }));
24686
+ global.__rpExit = () => engine.exit();
24536
24687
  if (initialPrompt) {
24537
24688
  await new Promise((r) => setTimeout(r, 200));
24538
- const ui = global.__rpUI;
24539
- ui?.addMessage(`\u276F ${initialPrompt}`);
24540
- ui?.setLoading(true);
24689
+ engine.ui?.addMessage(`\u276F ${initialPrompt}`);
24690
+ engine.ui?.setLoading(true);
24541
24691
  try {
24542
- const response = await handleMessage(initialPrompt);
24543
- if (response) ui?.addMessage(response);
24692
+ const response = await engine.handleAIMessage(initialPrompt);
24693
+ if (response) engine.ui?.addMessage(response);
24544
24694
  } finally {
24545
- ui?.setLoading(false);
24695
+ engine.ui?.setLoading(false);
24696
+ engine.updateStatus();
24546
24697
  }
24547
24698
  }
24548
24699
  await waitUntilExit();
24549
- saveSession(sessionId, sessionName, conversationMessages, usage);
24700
+ engine.exit();
24550
24701
  }
24551
24702
 
24552
24703
  // src/commands-modules.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robinpath/cli",
3
- "version": "1.81.0",
3
+ "version": "1.83.0",
4
4
  "description": "AI-powered scripting CLI — automate anything from your terminal",
5
5
  "type": "module",
6
6
  "license": "MIT",