@robota-sdk/agent-cli 3.0.0-beta.24 → 3.0.0-beta.26

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 { readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
3
- import { join as join3, dirname as dirname2 } from "path";
3
+ import { join as join5, dirname as dirname3 } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import {
6
6
  loadConfig,
@@ -157,7 +157,7 @@ import { getModelName } from "@robota-sdk/agent-core";
157
157
  import { useState, useCallback, useRef } from "react";
158
158
  import { createSession, FileSessionLogger, projectPaths } from "@robota-sdk/agent-sdk";
159
159
  var TOOL_ARG_DISPLAY_MAX = 80;
160
- var TOOL_ARG_TRUNCATE_AT = 77;
160
+ var TAIL_KEEP = 30;
161
161
  var NOOP_TERMINAL = {
162
162
  write: () => {
163
163
  },
@@ -216,13 +216,17 @@ function useSession(props) {
216
216
  if (event.toolArgs) {
217
217
  const firstVal = Object.values(event.toolArgs)[0];
218
218
  const raw = typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal ?? "");
219
- firstArg = raw.length > TOOL_ARG_DISPLAY_MAX ? raw.slice(0, TOOL_ARG_TRUNCATE_AT) + "..." : raw;
219
+ firstArg = raw.length > TOOL_ARG_DISPLAY_MAX ? raw.slice(0, TOOL_ARG_DISPLAY_MAX - TAIL_KEEP - 3) + "..." + raw.slice(-TAIL_KEEP) : raw;
220
220
  }
221
- setActiveTools((prev) => [...prev, { toolName: event.toolName, firstArg, isRunning: true }]);
221
+ setActiveTools((prev) => [
222
+ ...prev,
223
+ { toolName: event.toolName, firstArg, isRunning: true }
224
+ ]);
222
225
  } else {
226
+ const result = event.denied ? "denied" : event.success === false ? "error" : "success";
223
227
  setActiveTools(
224
228
  (prev) => prev.map(
225
- (t) => t.toolName === event.toolName && t.isRunning ? { ...t, isRunning: false } : t
229
+ (t) => t.toolName === event.toolName && t.isRunning ? { ...t, isRunning: false, result } : t
226
230
  )
227
231
  );
228
232
  }
@@ -246,7 +250,13 @@ function useSession(props) {
246
250
  setStreamingText("");
247
251
  setActiveTools([]);
248
252
  }, []);
249
- return { session: sessionRef.current, permissionRequest, streamingText, clearStreamingText, activeTools };
253
+ return {
254
+ session: sessionRef.current,
255
+ permissionRequest,
256
+ streamingText,
257
+ clearStreamingText,
258
+ activeTools
259
+ };
250
260
  }
251
261
 
252
262
  // src/ui/hooks/useMessages.ts
@@ -369,7 +379,122 @@ function handleReset(addMessage) {
369
379
  }
370
380
  return { handled: true, exitRequested: true };
371
381
  }
372
- async function executeSlashCommand(cmd, args, session, addMessage, clearMessages, registry) {
382
+ async function handlePluginCommand(args, addMessage, callbacks) {
383
+ const parts = args.trim().split(/\s+/);
384
+ const subcommand = parts[0] ?? "";
385
+ const subArgs = parts.slice(1).join(" ").trim();
386
+ try {
387
+ switch (subcommand) {
388
+ case "":
389
+ case void 0: {
390
+ const plugins = await callbacks.listInstalled();
391
+ if (plugins.length === 0) {
392
+ addMessage({ role: "system", content: "No plugins installed." });
393
+ } else {
394
+ const lines = plugins.map(
395
+ (p) => ` ${p.name} \u2014 ${p.description} [${p.enabled ? "enabled" : "disabled"}]`
396
+ );
397
+ addMessage({ role: "system", content: `Installed plugins:
398
+ ${lines.join("\n")}` });
399
+ }
400
+ return { handled: true };
401
+ }
402
+ case "install": {
403
+ if (!subArgs) {
404
+ addMessage({ role: "system", content: "Usage: /plugin install <name>@<marketplace>" });
405
+ return { handled: true };
406
+ }
407
+ await callbacks.install(subArgs);
408
+ addMessage({ role: "system", content: `Installed plugin: ${subArgs}` });
409
+ return { handled: true };
410
+ }
411
+ case "uninstall": {
412
+ if (!subArgs) {
413
+ addMessage({ role: "system", content: "Usage: /plugin uninstall <name>@<marketplace>" });
414
+ return { handled: true };
415
+ }
416
+ await callbacks.uninstall(subArgs);
417
+ addMessage({ role: "system", content: `Uninstalled plugin: ${subArgs}` });
418
+ return { handled: true };
419
+ }
420
+ case "enable": {
421
+ if (!subArgs) {
422
+ addMessage({ role: "system", content: "Usage: /plugin enable <name>@<marketplace>" });
423
+ return { handled: true };
424
+ }
425
+ await callbacks.enable(subArgs);
426
+ addMessage({ role: "system", content: `Enabled plugin: ${subArgs}` });
427
+ return { handled: true };
428
+ }
429
+ case "disable": {
430
+ if (!subArgs) {
431
+ addMessage({ role: "system", content: "Usage: /plugin disable <name>@<marketplace>" });
432
+ return { handled: true };
433
+ }
434
+ await callbacks.disable(subArgs);
435
+ addMessage({ role: "system", content: `Disabled plugin: ${subArgs}` });
436
+ return { handled: true };
437
+ }
438
+ case "marketplace": {
439
+ const mpParts = subArgs.split(/\s+/);
440
+ const mpSubcommand = mpParts[0] ?? "";
441
+ const mpArgs = mpParts.slice(1).join(" ").trim();
442
+ if (mpSubcommand === "add" && mpArgs) {
443
+ const registeredName = await callbacks.marketplaceAdd(mpArgs);
444
+ addMessage({
445
+ role: "system",
446
+ content: `Added marketplace: "${registeredName}" (from ${mpArgs})
447
+ Install plugins with: /plugin install <name>@${registeredName}`
448
+ });
449
+ return { handled: true };
450
+ } else if (mpSubcommand === "remove" && mpArgs) {
451
+ await callbacks.marketplaceRemove(mpArgs);
452
+ addMessage({
453
+ role: "system",
454
+ content: `Removed marketplace "${mpArgs}" and uninstalled its plugins.`
455
+ });
456
+ return { handled: true };
457
+ } else if (mpSubcommand === "update" && mpArgs) {
458
+ await callbacks.marketplaceUpdate(mpArgs);
459
+ addMessage({
460
+ role: "system",
461
+ content: `Updated marketplace "${mpArgs}".`
462
+ });
463
+ return { handled: true };
464
+ } else if (mpSubcommand === "list") {
465
+ const sources = await callbacks.marketplaceList();
466
+ if (sources.length === 0) {
467
+ addMessage({ role: "system", content: "No marketplace sources configured." });
468
+ } else {
469
+ const lines = sources.map((s) => ` ${s.name} (${s.type})`);
470
+ addMessage({ role: "system", content: `Marketplace sources:
471
+ ${lines.join("\n")}` });
472
+ }
473
+ return { handled: true };
474
+ } else {
475
+ addMessage({
476
+ role: "system",
477
+ content: "Usage: /plugin marketplace add <source> | remove <name> | update <name> | list"
478
+ });
479
+ return { handled: true };
480
+ }
481
+ }
482
+ default:
483
+ addMessage({ role: "system", content: `Unknown plugin subcommand: ${subcommand}` });
484
+ return { handled: true };
485
+ }
486
+ } catch (error) {
487
+ const message = error instanceof Error ? error.message : String(error);
488
+ addMessage({ role: "system", content: `Plugin error: ${message}` });
489
+ return { handled: true };
490
+ }
491
+ }
492
+ async function handleReloadPlugins(addMessage, callbacks) {
493
+ await callbacks.reloadPlugins();
494
+ addMessage({ role: "system", content: "Plugins reload complete." });
495
+ return { handled: true };
496
+ }
497
+ async function executeSlashCommand(cmd, args, session, addMessage, clearMessages, registry, pluginCallbacks) {
373
498
  switch (cmd) {
374
499
  case "help":
375
500
  return handleHelp(addMessage);
@@ -393,10 +518,22 @@ async function executeSlashCommand(cmd, args, session, addMessage, clearMessages
393
518
  return handleReset(addMessage);
394
519
  case "exit":
395
520
  return { handled: true, exitRequested: true };
521
+ case "plugin":
522
+ if (pluginCallbacks) {
523
+ return handlePluginCommand(args, addMessage, pluginCallbacks);
524
+ }
525
+ addMessage({ role: "system", content: "Plugin management is not available." });
526
+ return { handled: true };
527
+ case "reload-plugins":
528
+ if (pluginCallbacks) {
529
+ return handleReloadPlugins(addMessage, pluginCallbacks);
530
+ }
531
+ addMessage({ role: "system", content: "Plugin management is not available." });
532
+ return { handled: true };
396
533
  default: {
397
- const skillCmd = registry.getCommands().find((c) => c.name === cmd && c.source === "skill");
398
- if (skillCmd) {
399
- addMessage({ role: "system", content: `Invoking skill: ${cmd}` });
534
+ const dynamicCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
535
+ if (dynamicCmd) {
536
+ addMessage({ role: "system", content: `Invoking ${dynamicCmd.source}: ${cmd}` });
400
537
  return { handled: false };
401
538
  }
402
539
  addMessage({ role: "system", content: `Unknown command "/${cmd}". Type /help for help.` });
@@ -407,14 +544,22 @@ async function executeSlashCommand(cmd, args, session, addMessage, clearMessages
407
544
 
408
545
  // src/ui/hooks/useSlashCommands.ts
409
546
  var EXIT_DELAY_MS = 500;
410
- function useSlashCommands(session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId) {
547
+ function useSlashCommands(session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId, pluginCallbacks) {
411
548
  return useCallback3(
412
549
  async (input) => {
413
550
  const parts = input.slice(1).split(/\s+/);
414
551
  const cmd = parts[0]?.toLowerCase() ?? "";
415
552
  const args = parts.slice(1).join(" ");
416
553
  const clearMessages = () => setMessages([]);
417
- const result = await executeSlashCommand(cmd, args, session, addMessage, clearMessages, registry);
554
+ const result = await executeSlashCommand(
555
+ cmd,
556
+ args,
557
+ session,
558
+ addMessage,
559
+ clearMessages,
560
+ registry,
561
+ pluginCallbacks
562
+ );
418
563
  if (result.pendingModelId) {
419
564
  pendingModelChangeRef.current = result.pendingModelId;
420
565
  setPendingModelId(result.pendingModelId);
@@ -424,7 +569,16 @@ function useSlashCommands(session, addMessage, setMessages, exit, registry, pend
424
569
  }
425
570
  return result.handled;
426
571
  },
427
- [session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId]
572
+ [
573
+ session,
574
+ addMessage,
575
+ setMessages,
576
+ exit,
577
+ registry,
578
+ pendingModelChangeRef,
579
+ setPendingModelId,
580
+ pluginCallbacks
581
+ ]
428
582
  );
429
583
  }
430
584
 
@@ -433,7 +587,7 @@ import { useCallback as useCallback4 } from "react";
433
587
 
434
588
  // src/utils/tool-call-extractor.ts
435
589
  var TOOL_ARG_MAX_LENGTH = 80;
436
- var TOOL_ARG_TRUNCATE_LENGTH = 77;
590
+ var TAIL_KEEP2 = 30;
437
591
  function extractToolCalls(history, startIndex) {
438
592
  const lines = [];
439
593
  for (let i = startIndex; i < history.length; i++) {
@@ -441,7 +595,7 @@ function extractToolCalls(history, startIndex) {
441
595
  if (msg.role === "assistant" && msg.toolCalls) {
442
596
  for (const tc of msg.toolCalls) {
443
597
  const value = parseFirstArgValue(tc.function.arguments);
444
- const truncated = value.length > TOOL_ARG_MAX_LENGTH ? value.slice(0, TOOL_ARG_TRUNCATE_LENGTH) + "..." : value;
598
+ const truncated = value.length > TOOL_ARG_MAX_LENGTH ? value.slice(0, TOOL_ARG_MAX_LENGTH - TAIL_KEEP2 - 3) + "..." + value.slice(-TAIL_KEEP2) : value;
445
599
  lines.push(`${tc.function.name}(${truncated})`);
446
600
  }
447
601
  }
@@ -459,16 +613,60 @@ function parseFirstArgValue(argsJson) {
459
613
  }
460
614
 
461
615
  // src/utils/skill-prompt.ts
462
- function buildSkillPrompt(input, registry) {
616
+ import { execSync } from "child_process";
617
+ function substituteVariables(content, args, context) {
618
+ const argParts = args ? args.split(/\s+/) : [];
619
+ let result = content;
620
+ result = result.replace(/\$ARGUMENTS\[(\d+)]/g, (_match, index) => {
621
+ return argParts[Number(index)] ?? "";
622
+ });
623
+ result = result.replace(/\$ARGUMENTS/g, args);
624
+ result = result.replace(/\$(\d)(?!\d|\w|\[)/g, (_match, digit) => {
625
+ return argParts[Number(digit)] ?? "";
626
+ });
627
+ result = result.replace(/\$\{CLAUDE_SESSION_ID}/g, context?.sessionId ?? "");
628
+ result = result.replace(/\$\{CLAUDE_SKILL_DIR}/g, context?.skillDir ?? "");
629
+ return result;
630
+ }
631
+ async function preprocessShellCommands(content) {
632
+ const shellPattern = /!`([^`]+)`/g;
633
+ if (!shellPattern.test(content)) {
634
+ return content;
635
+ }
636
+ shellPattern.lastIndex = 0;
637
+ let result = content;
638
+ let match;
639
+ const matches = [];
640
+ while ((match = shellPattern.exec(content)) !== null) {
641
+ matches.push({ full: match[0], command: match[1] });
642
+ }
643
+ for (const { full, command } of matches) {
644
+ let output = "";
645
+ try {
646
+ output = execSync(command, {
647
+ timeout: 5e3,
648
+ encoding: "utf-8",
649
+ stdio: ["pipe", "pipe", "pipe"]
650
+ }).trimEnd();
651
+ } catch {
652
+ output = "";
653
+ }
654
+ result = result.replace(full, output);
655
+ }
656
+ return result;
657
+ }
658
+ async function buildSkillPrompt(input, registry, context) {
463
659
  const parts = input.slice(1).split(/\s+/);
464
660
  const cmd = parts[0]?.toLowerCase() ?? "";
465
- const skillCmd = registry.getCommands().find((c) => c.name === cmd && c.source === "skill");
661
+ const skillCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
466
662
  if (!skillCmd) return null;
467
663
  const args = parts.slice(1).join(" ").trim();
468
664
  const userInstruction = args || skillCmd.description;
469
665
  if (skillCmd.skillContent) {
666
+ let processed = await preprocessShellCommands(skillCmd.skillContent);
667
+ processed = substituteVariables(processed, args, context);
470
668
  return `<skill name="${cmd}">
471
- ${skillCmd.skillContent}
669
+ ${processed}
472
670
  </skill>
473
671
 
474
672
  Execute the "${cmd}" skill: ${userInstruction}`;
@@ -481,12 +679,12 @@ function syncContextState(session, setter) {
481
679
  const ctx = session.getContextState();
482
680
  setter({ percentage: ctx.usedPercentage, usedTokens: ctx.usedTokens, maxTokens: ctx.maxTokens });
483
681
  }
484
- async function runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextState) {
682
+ async function runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextState, rawInput) {
485
683
  setIsThinking(true);
486
684
  clearStreamingText();
487
685
  const historyBefore = session.getHistory().length;
488
686
  try {
489
- const response = await session.run(prompt);
687
+ const response = await session.run(prompt, rawInput);
490
688
  clearStreamingText();
491
689
  const history = session.getHistory();
492
690
  const toolLines = extractToolCalls(
@@ -494,7 +692,11 @@ async function runSessionPrompt(prompt, session, addMessage, clearStreamingText,
494
692
  historyBefore
495
693
  );
496
694
  if (toolLines.length > 0) {
497
- addMessage({ role: "tool", content: toolLines.join("\n"), toolName: `${toolLines.length} tools` });
695
+ addMessage({
696
+ role: "tool",
697
+ content: toolLines.join("\n"),
698
+ toolName: `${toolLines.length} tools`
699
+ });
498
700
  }
499
701
  addMessage({ role: "assistant", content: response || "(empty response)" });
500
702
  syncContextState(session, setContextState);
@@ -519,19 +721,48 @@ function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamin
519
721
  syncContextState(session, setContextState);
520
722
  return;
521
723
  }
522
- const prompt = buildSkillPrompt(input, registry);
724
+ const prompt = await buildSkillPrompt(input, registry);
523
725
  if (!prompt) return;
524
- return runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextState);
726
+ const cmdName = input.slice(1).split(/\s+/)[0]?.toLowerCase() ?? "";
727
+ const qualifiedName = registry.resolveQualifiedName(cmdName);
728
+ const hookInput = qualifiedName ? `/${qualifiedName}${input.slice(1 + cmdName.length)}` : input;
729
+ return runSessionPrompt(
730
+ prompt,
731
+ session,
732
+ addMessage,
733
+ clearStreamingText,
734
+ setIsThinking,
735
+ setContextState,
736
+ hookInput
737
+ );
525
738
  }
526
739
  addMessage({ role: "user", content: input });
527
- return runSessionPrompt(input, session, addMessage, clearStreamingText, setIsThinking, setContextState);
740
+ return runSessionPrompt(
741
+ input,
742
+ session,
743
+ addMessage,
744
+ clearStreamingText,
745
+ setIsThinking,
746
+ setContextState
747
+ );
528
748
  },
529
- [session, addMessage, handleSlashCommand, clearStreamingText, setIsThinking, setContextState, registry]
749
+ [
750
+ session,
751
+ addMessage,
752
+ handleSlashCommand,
753
+ clearStreamingText,
754
+ setIsThinking,
755
+ setContextState,
756
+ registry
757
+ ]
530
758
  );
531
759
  }
532
760
 
533
761
  // src/ui/hooks/useCommandRegistry.ts
534
762
  import { useRef as useRef2 } from "react";
763
+ import { homedir as homedir2 } from "os";
764
+ import { join as join3, dirname as dirname2 } from "path";
765
+ import { BundlePluginLoader } from "@robota-sdk/agent-sdk";
535
766
 
536
767
  // src/commands/command-registry.ts
537
768
  var CommandRegistry = class {
@@ -549,6 +780,14 @@ var CommandRegistry = class {
549
780
  const lower = filter.toLowerCase();
550
781
  return all.filter((cmd) => cmd.name.toLowerCase().startsWith(lower));
551
782
  }
783
+ /** Resolve a short name to its fully qualified plugin:name form */
784
+ resolveQualifiedName(shortName) {
785
+ const matches = this.getCommands().filter(
786
+ (c) => c.source === "plugin" && c.name.includes(":") && c.name.endsWith(`:${shortName}`)
787
+ );
788
+ if (matches.length !== 1) return null;
789
+ return matches[0].name;
790
+ }
552
791
  /** Get subcommands for a specific command */
553
792
  getSubcommands(commandName) {
554
793
  const lower = commandName.toLowerCase();
@@ -615,6 +854,23 @@ function createBuiltinCommands() {
615
854
  { name: "cost", description: "Show session info", source: "builtin" },
616
855
  { name: "context", description: "Context window info", source: "builtin" },
617
856
  { name: "permissions", description: "Permission rules", source: "builtin" },
857
+ {
858
+ name: "plugin",
859
+ description: "Manage plugins",
860
+ source: "builtin",
861
+ subcommands: [
862
+ { name: "install", description: "Install a plugin (name@marketplace)", source: "builtin" },
863
+ {
864
+ name: "uninstall",
865
+ description: "Uninstall a plugin (name@marketplace)",
866
+ source: "builtin"
867
+ },
868
+ { name: "enable", description: "Enable a plugin (name@marketplace)", source: "builtin" },
869
+ { name: "disable", description: "Disable a plugin (name@marketplace)", source: "builtin" },
870
+ { name: "marketplace", description: "Manage marketplace sources", source: "builtin" }
871
+ ]
872
+ },
873
+ { name: "reload-plugins", description: "Reload all plugin resources", source: "builtin" },
618
874
  { name: "reset", description: "Delete settings and exit", source: "builtin" },
619
875
  { name: "exit", description: "Exit CLI", source: "builtin" }
620
876
  ];
@@ -632,27 +888,52 @@ var BuiltinCommandSource = class {
632
888
 
633
889
  // src/commands/skill-source.ts
634
890
  import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
635
- import { join as join2 } from "path";
891
+ import { join as join2, basename } from "path";
636
892
  import { homedir } from "os";
893
+ var BOOLEAN_KEYS = /* @__PURE__ */ new Set(["disable-model-invocation", "user-invocable"]);
894
+ var LIST_KEYS = /* @__PURE__ */ new Set(["allowed-tools"]);
895
+ function kebabToCamel(key) {
896
+ return key.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
897
+ }
637
898
  function parseFrontmatter(content) {
638
899
  const lines = content.split("\n");
639
900
  if (lines[0]?.trim() !== "---") return null;
640
- let name = "";
641
- let description = "";
901
+ const result = {};
642
902
  for (let i = 1; i < lines.length; i++) {
643
903
  const line = lines[i];
644
904
  if (line.trim() === "---") break;
645
- const nameMatch = line.match(/^name:\s*(.+)/);
646
- if (nameMatch) {
647
- name = nameMatch[1].trim();
648
- continue;
649
- }
650
- const descMatch = line.match(/^description:\s*(.+)/);
651
- if (descMatch) {
652
- description = descMatch[1].trim();
905
+ const match = line.match(/^([a-z][a-z0-9-]*):\s*(.+)/);
906
+ if (!match) continue;
907
+ const key = match[1];
908
+ const rawValue = match[2].trim();
909
+ const camelKey = kebabToCamel(key);
910
+ if (BOOLEAN_KEYS.has(key)) {
911
+ result[camelKey] = rawValue === "true";
912
+ } else if (LIST_KEYS.has(key)) {
913
+ result[camelKey] = rawValue.split(",").map((s) => s.trim());
914
+ } else {
915
+ result[camelKey] = rawValue;
653
916
  }
654
917
  }
655
- return name ? { name, description } : null;
918
+ return Object.keys(result).length > 0 ? result : null;
919
+ }
920
+ function buildCommand(frontmatter, content, fallbackName) {
921
+ const cmd = {
922
+ name: frontmatter?.name ?? fallbackName,
923
+ description: frontmatter?.description ?? `Skill: ${fallbackName}`,
924
+ source: "skill",
925
+ skillContent: content
926
+ };
927
+ if (frontmatter?.argumentHint !== void 0) cmd.argumentHint = frontmatter.argumentHint;
928
+ if (frontmatter?.disableModelInvocation !== void 0)
929
+ cmd.disableModelInvocation = frontmatter.disableModelInvocation;
930
+ if (frontmatter?.userInvocable !== void 0) cmd.userInvocable = frontmatter.userInvocable;
931
+ if (frontmatter?.allowedTools !== void 0) cmd.allowedTools = frontmatter.allowedTools;
932
+ if (frontmatter?.model !== void 0) cmd.model = frontmatter.model;
933
+ if (frontmatter?.effort !== void 0) cmd.effort = frontmatter.effort;
934
+ if (frontmatter?.context !== void 0) cmd.context = frontmatter.context;
935
+ if (frontmatter?.agent !== void 0) cmd.agent = frontmatter.agent;
936
+ return cmd;
656
937
  }
657
938
  function scanSkillsDir(skillsDir) {
658
939
  if (!existsSync2(skillsDir)) return [];
@@ -664,48 +945,251 @@ function scanSkillsDir(skillsDir) {
664
945
  if (!existsSync2(skillFile)) continue;
665
946
  const content = readFileSync2(skillFile, "utf-8");
666
947
  const frontmatter = parseFrontmatter(content);
667
- commands.push({
668
- name: frontmatter?.name ?? entry.name,
669
- description: frontmatter?.description ?? `Skill: ${entry.name}`,
670
- source: "skill",
671
- skillContent: content
672
- });
948
+ commands.push(buildCommand(frontmatter, content, entry.name));
949
+ }
950
+ return commands;
951
+ }
952
+ function scanCommandsDir(commandsDir) {
953
+ if (!existsSync2(commandsDir)) return [];
954
+ const commands = [];
955
+ const entries = readdirSync(commandsDir, { withFileTypes: true });
956
+ for (const entry of entries) {
957
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
958
+ const filePath = join2(commandsDir, entry.name);
959
+ const content = readFileSync2(filePath, "utf-8");
960
+ const frontmatter = parseFrontmatter(content);
961
+ const fallbackName = basename(entry.name, ".md");
962
+ commands.push(buildCommand(frontmatter, content, fallbackName));
673
963
  }
674
964
  return commands;
675
965
  }
676
966
  var SkillCommandSource = class {
677
967
  name = "skill";
678
968
  cwd;
969
+ home;
679
970
  cachedCommands = null;
680
- constructor(cwd) {
971
+ constructor(cwd, home) {
681
972
  this.cwd = cwd;
973
+ this.home = home ?? homedir();
682
974
  }
683
975
  getCommands() {
684
976
  if (this.cachedCommands) return this.cachedCommands;
685
- const projectSkills = scanSkillsDir(join2(this.cwd, ".agents", "skills"));
686
- const userSkills = scanSkillsDir(join2(homedir(), ".claude", "skills"));
687
- const seen = new Set(projectSkills.map((cmd) => cmd.name));
688
- const merged = [...projectSkills];
689
- for (const cmd of userSkills) {
690
- if (!seen.has(cmd.name)) {
691
- merged.push(cmd);
977
+ const sources = [
978
+ scanSkillsDir(join2(this.cwd, ".claude", "skills")),
979
+ // 1. project .claude/skills
980
+ scanCommandsDir(join2(this.cwd, ".claude", "commands")),
981
+ // 2. project .claude/commands (legacy)
982
+ scanSkillsDir(join2(this.home, ".robota", "skills")),
983
+ // 3. user ~/.robota/skills
984
+ scanSkillsDir(join2(this.cwd, ".agents", "skills"))
985
+ // 4. project .agents/skills
986
+ ];
987
+ const seen = /* @__PURE__ */ new Set();
988
+ const merged = [];
989
+ for (const commands of sources) {
990
+ for (const cmd of commands) {
991
+ if (!seen.has(cmd.name)) {
992
+ seen.add(cmd.name);
993
+ merged.push(cmd);
994
+ }
692
995
  }
693
996
  }
694
997
  this.cachedCommands = merged;
695
998
  return this.cachedCommands;
696
999
  }
1000
+ /** Get skills that models can invoke (excludes disableModelInvocation: true) */
1001
+ getModelInvocableSkills() {
1002
+ return this.getCommands().filter((cmd) => cmd.disableModelInvocation !== true);
1003
+ }
1004
+ /** Get skills that users can invoke (excludes userInvocable: false) */
1005
+ getUserInvocableSkills() {
1006
+ return this.getCommands().filter((cmd) => cmd.userInvocable !== false);
1007
+ }
1008
+ };
1009
+
1010
+ // src/commands/plugin-source.ts
1011
+ var PluginCommandSource = class {
1012
+ name = "plugin";
1013
+ plugins;
1014
+ constructor(plugins) {
1015
+ this.plugins = plugins;
1016
+ }
1017
+ getCommands() {
1018
+ const commands = [];
1019
+ for (const plugin of this.plugins) {
1020
+ for (const skill of plugin.skills) {
1021
+ const baseName = skill.name.includes("@") ? skill.name.split("@")[0] : skill.name;
1022
+ commands.push({
1023
+ name: baseName,
1024
+ description: `(${plugin.manifest.name}) ${skill.description}`,
1025
+ source: "plugin",
1026
+ skillContent: skill.skillContent,
1027
+ pluginDir: plugin.pluginDir
1028
+ });
1029
+ }
1030
+ for (const cmd of plugin.commands) {
1031
+ commands.push({
1032
+ name: cmd.name,
1033
+ description: cmd.description,
1034
+ source: "plugin",
1035
+ skillContent: cmd.skillContent,
1036
+ pluginDir: plugin.pluginDir
1037
+ });
1038
+ }
1039
+ }
1040
+ return commands;
1041
+ }
697
1042
  };
698
1043
 
699
1044
  // src/ui/hooks/useCommandRegistry.ts
1045
+ function buildPluginEnv(plugin) {
1046
+ const dataDir = join3(dirname2(dirname2(plugin.pluginDir)), "data", plugin.manifest.name);
1047
+ return {
1048
+ CLAUDE_PLUGIN_ROOT: plugin.pluginDir,
1049
+ CLAUDE_PLUGIN_PATH: plugin.pluginDir,
1050
+ CLAUDE_PLUGIN_DATA: dataDir
1051
+ };
1052
+ }
1053
+ function mergePluginHooks(plugins) {
1054
+ const merged = {};
1055
+ for (const plugin of plugins) {
1056
+ const hooksObj = plugin.hooks;
1057
+ if (!hooksObj) continue;
1058
+ const pluginEnv = buildPluginEnv(plugin);
1059
+ const innerHooks = hooksObj.hooks ?? hooksObj;
1060
+ for (const [event, groups] of Object.entries(innerHooks)) {
1061
+ if (!Array.isArray(groups)) continue;
1062
+ if (!merged[event]) merged[event] = [];
1063
+ const resolved = groups.map((group) => {
1064
+ const resolved2 = resolvePluginRoot(group, plugin.pluginDir);
1065
+ if (typeof resolved2 === "object" && resolved2 !== null) {
1066
+ resolved2.env = pluginEnv;
1067
+ }
1068
+ return resolved2;
1069
+ });
1070
+ merged[event].push(...resolved);
1071
+ }
1072
+ }
1073
+ return merged;
1074
+ }
1075
+ function resolvePluginRoot(group, pluginDir) {
1076
+ if (typeof group !== "object" || group === null) return group;
1077
+ const obj = group;
1078
+ if (Array.isArray(obj.hooks)) {
1079
+ return {
1080
+ ...obj,
1081
+ hooks: obj.hooks.map((h) => {
1082
+ if (typeof h !== "object" || h === null) return h;
1083
+ const hook = h;
1084
+ if (typeof hook.command === "string") {
1085
+ return {
1086
+ ...hook,
1087
+ command: hook.command.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pluginDir)
1088
+ };
1089
+ }
1090
+ return hook;
1091
+ })
1092
+ };
1093
+ }
1094
+ return group;
1095
+ }
700
1096
  function useCommandRegistry(cwd) {
701
- const registryRef = useRef2(null);
702
- if (registryRef.current === null) {
1097
+ const resultRef = useRef2(null);
1098
+ if (resultRef.current === null) {
703
1099
  const registry = new CommandRegistry();
704
1100
  registry.addSource(new BuiltinCommandSource());
705
1101
  registry.addSource(new SkillCommandSource(cwd));
706
- registryRef.current = registry;
1102
+ let pluginHooks = {};
1103
+ const pluginsDir = join3(homedir2(), ".robota", "plugins");
1104
+ const loader = new BundlePluginLoader(pluginsDir);
1105
+ try {
1106
+ const plugins = loader.loadPluginsSync();
1107
+ if (plugins.length > 0) {
1108
+ registry.addSource(new PluginCommandSource(plugins));
1109
+ pluginHooks = mergePluginHooks(plugins);
1110
+ }
1111
+ } catch {
1112
+ }
1113
+ resultRef.current = { registry, pluginHooks };
707
1114
  }
708
- return registryRef.current;
1115
+ return resultRef.current;
1116
+ }
1117
+
1118
+ // src/ui/hooks/usePluginCallbacks.ts
1119
+ import { useMemo } from "react";
1120
+ import { homedir as homedir3 } from "os";
1121
+ import { join as join4 } from "path";
1122
+ import {
1123
+ PluginSettingsStore,
1124
+ BundlePluginLoader as BundlePluginLoader2,
1125
+ BundlePluginInstaller,
1126
+ MarketplaceClient
1127
+ } from "@robota-sdk/agent-sdk";
1128
+ function usePluginCallbacks(cwd) {
1129
+ return useMemo(() => {
1130
+ const home = homedir3();
1131
+ const pluginsDir = join4(home, ".robota", "plugins");
1132
+ const userSettingsPath = join4(home, ".robota", "settings.json");
1133
+ const settingsStore = new PluginSettingsStore(userSettingsPath);
1134
+ const marketplace = new MarketplaceClient({ pluginsDir });
1135
+ const installer = new BundlePluginInstaller({
1136
+ pluginsDir,
1137
+ settingsStore,
1138
+ marketplaceClient: marketplace
1139
+ });
1140
+ const loader = new BundlePluginLoader2(pluginsDir);
1141
+ return {
1142
+ listInstalled: async () => {
1143
+ const plugins = await loader.loadAll();
1144
+ return plugins.map((p) => ({
1145
+ name: p.manifest.name,
1146
+ description: p.manifest.description,
1147
+ enabled: true
1148
+ }));
1149
+ },
1150
+ install: async (pluginId) => {
1151
+ const [name, marketplaceName] = pluginId.split("@");
1152
+ if (!name || !marketplaceName) {
1153
+ throw new Error("Plugin ID must be in format: name@marketplace");
1154
+ }
1155
+ await installer.install(name, marketplaceName);
1156
+ },
1157
+ uninstall: async (pluginId) => {
1158
+ await installer.uninstall(pluginId);
1159
+ },
1160
+ enable: async (pluginId) => {
1161
+ await installer.enable(pluginId);
1162
+ },
1163
+ disable: async (pluginId) => {
1164
+ await installer.disable(pluginId);
1165
+ },
1166
+ marketplaceAdd: async (source) => {
1167
+ if (source.includes("/") && !source.includes(":")) {
1168
+ return marketplace.addMarketplace({ type: "github", repo: source });
1169
+ } else {
1170
+ return marketplace.addMarketplace({ type: "git", url: source });
1171
+ }
1172
+ },
1173
+ marketplaceRemove: async (name) => {
1174
+ const installedFromMarketplace = installer.getPluginsByMarketplace(name);
1175
+ for (const record of installedFromMarketplace) {
1176
+ await installer.uninstall(`${record.pluginName}@${record.marketplace}`);
1177
+ }
1178
+ marketplace.removeMarketplace(name);
1179
+ },
1180
+ marketplaceUpdate: async (name) => {
1181
+ marketplace.updateMarketplace(name);
1182
+ },
1183
+ marketplaceList: async () => {
1184
+ return marketplace.listMarketplaces().map((m) => ({
1185
+ name: m.name,
1186
+ type: m.source.type
1187
+ }));
1188
+ },
1189
+ reloadPlugins: async () => {
1190
+ }
1191
+ };
1192
+ }, [cwd]);
709
1193
  }
710
1194
 
711
1195
  // src/ui/MessageList.tsx
@@ -742,13 +1226,39 @@ function RoleLabel({ role }) {
742
1226
  " "
743
1227
  ] });
744
1228
  case "tool":
745
- return /* @__PURE__ */ jsxs(Text, { color: "magenta", bold: true, children: [
1229
+ return /* @__PURE__ */ jsxs(Text, { color: "white", bold: true, children: [
746
1230
  "Tool:",
747
1231
  " "
748
1232
  ] });
749
1233
  }
750
1234
  }
1235
+ function ToolMessage({ message }) {
1236
+ const lines = message.content.split("\n").filter((l) => l.trim());
1237
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
1238
+ /* @__PURE__ */ jsxs(Box, { children: [
1239
+ /* @__PURE__ */ jsxs(Text, { color: "white", bold: true, children: [
1240
+ "Tool:",
1241
+ " "
1242
+ ] }),
1243
+ message.toolName && /* @__PURE__ */ jsxs(Text, { color: "white", dimColor: true, children: [
1244
+ "[",
1245
+ message.toolName,
1246
+ "]"
1247
+ ] })
1248
+ ] }),
1249
+ /* @__PURE__ */ jsx(Text, { children: " " }),
1250
+ lines.map((line, i) => /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1251
+ " ",
1252
+ "\u2713",
1253
+ " ",
1254
+ line
1255
+ ] }, i))
1256
+ ] });
1257
+ }
751
1258
  function MessageItem({ message }) {
1259
+ if (message.role === "tool") {
1260
+ return /* @__PURE__ */ jsx(ToolMessage, { message });
1261
+ }
752
1262
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
753
1263
  /* @__PURE__ */ jsxs(Box, { children: [
754
1264
  /* @__PURE__ */ jsx(RoleLabel, { role: message.role }),
@@ -828,7 +1338,7 @@ function StatusBar({
828
1338
  }
829
1339
 
830
1340
  // src/ui/InputArea.tsx
831
- import React3, { useState as useState5, useCallback as useCallback5, useMemo } from "react";
1341
+ import React4, { useState as useState5, useCallback as useCallback5, useMemo as useMemo2 } from "react";
832
1342
  import { Box as Box4, Text as Text6, useInput as useInput2 } from "ink";
833
1343
 
834
1344
  // src/ui/CjkTextInput.tsx
@@ -1007,14 +1517,14 @@ function parseSlashInput(value) {
1007
1517
  function useAutocomplete(value, registry) {
1008
1518
  const [selectedIndex, setSelectedIndex] = useState5(0);
1009
1519
  const [dismissed, setDismissed] = useState5(false);
1010
- const prevValueRef = React3.useRef(value);
1520
+ const prevValueRef = React4.useRef(value);
1011
1521
  if (prevValueRef.current !== value) {
1012
1522
  prevValueRef.current = value;
1013
1523
  if (dismissed) setDismissed(false);
1014
1524
  }
1015
1525
  const parsed = parseSlashInput(value);
1016
1526
  const isSubcommandMode = parsed.isSlash && parsed.parentCommand.length > 0;
1017
- const filteredCommands = useMemo(() => {
1527
+ const filteredCommands = useMemo2(() => {
1018
1528
  if (!registry || !parsed.isSlash || dismissed) return [];
1019
1529
  if (isSubcommandMode) {
1020
1530
  const subs = registry.getSubcommands(parsed.parentCommand);
@@ -1174,7 +1684,7 @@ function ConfirmPrompt({
1174
1684
  }
1175
1685
 
1176
1686
  // src/ui/PermissionPrompt.tsx
1177
- import React5 from "react";
1687
+ import React6 from "react";
1178
1688
  import { Box as Box6, Text as Text8, useInput as useInput4 } from "ink";
1179
1689
  import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1180
1690
  var OPTIONS = ["Allow", "Allow always (this session)", "Deny"];
@@ -1184,15 +1694,15 @@ function formatArgs(args) {
1184
1694
  return entries.map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`).join(", ");
1185
1695
  }
1186
1696
  function PermissionPrompt({ request }) {
1187
- const [selected, setSelected] = React5.useState(0);
1188
- const resolvedRef = React5.useRef(false);
1189
- const prevRequestRef = React5.useRef(request);
1697
+ const [selected, setSelected] = React6.useState(0);
1698
+ const resolvedRef = React6.useRef(false);
1699
+ const prevRequestRef = React6.useRef(request);
1190
1700
  if (prevRequestRef.current !== request) {
1191
1701
  prevRequestRef.current = request;
1192
1702
  resolvedRef.current = false;
1193
1703
  setSelected(0);
1194
1704
  }
1195
- const doResolve = React5.useCallback(
1705
+ const doResolve = React6.useCallback(
1196
1706
  (index) => {
1197
1707
  if (resolvedRef.current) return;
1198
1708
  resolvedRef.current = true;
@@ -1240,6 +1750,12 @@ function PermissionPrompt({ request }) {
1240
1750
  // src/ui/StreamingIndicator.tsx
1241
1751
  import { Box as Box7, Text as Text9 } from "ink";
1242
1752
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1753
+ function getToolStyle(t) {
1754
+ if (t.isRunning) return { color: "yellow", icon: "\u27F3", strikethrough: false };
1755
+ if (t.result === "error") return { color: "red", icon: "\u2717", strikethrough: true };
1756
+ if (t.result === "denied") return { color: "yellowBright", icon: "\u2298", strikethrough: true };
1757
+ return { color: "green", icon: "\u2713", strikethrough: false };
1758
+ }
1243
1759
  function StreamingIndicator({ text, activeTools }) {
1244
1760
  const hasTools = activeTools.length > 0;
1245
1761
  const hasText = text.length > 0;
@@ -1248,17 +1764,20 @@ function StreamingIndicator({ text, activeTools }) {
1248
1764
  }
1249
1765
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1250
1766
  hasTools && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
1251
- /* @__PURE__ */ jsx9(Text9, { color: "gray", bold: true, children: "Tools:" }),
1767
+ /* @__PURE__ */ jsx9(Text9, { color: "white", bold: true, children: "Tools:" }),
1252
1768
  /* @__PURE__ */ jsx9(Text9, { children: " " }),
1253
- activeTools.map((t, i) => /* @__PURE__ */ jsxs7(Text9, { color: t.isRunning ? "yellow" : "green", children: [
1254
- " ",
1255
- t.isRunning ? "\u27F3" : "\u2713",
1256
- " ",
1257
- t.toolName,
1258
- "(",
1259
- t.firstArg,
1260
- ")"
1261
- ] }, `${t.toolName}-${i}`))
1769
+ activeTools.map((t, i) => {
1770
+ const { color, icon, strikethrough } = getToolStyle(t);
1771
+ return /* @__PURE__ */ jsxs7(Text9, { color, strikethrough, children: [
1772
+ " ",
1773
+ icon,
1774
+ " ",
1775
+ t.toolName,
1776
+ "(",
1777
+ t.firstArg,
1778
+ ")"
1779
+ ] }, `${t.toolName}-${i}`);
1780
+ })
1262
1781
  ] }),
1263
1782
  hasText && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
1264
1783
  /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: "Robota:" }),
@@ -1271,9 +1790,35 @@ function StreamingIndicator({ text, activeTools }) {
1271
1790
  // src/ui/App.tsx
1272
1791
  import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1273
1792
  var EXIT_DELAY_MS2 = 500;
1793
+ function mergeHooksIntoConfig(configHooks, pluginHooks) {
1794
+ const pluginKeys = Object.keys(pluginHooks);
1795
+ if (pluginKeys.length === 0) return configHooks;
1796
+ const merged = {};
1797
+ for (const [event, groups] of Object.entries(pluginHooks)) {
1798
+ merged[event] = [...groups];
1799
+ }
1800
+ if (configHooks) {
1801
+ for (const [event, groups] of Object.entries(configHooks)) {
1802
+ if (!Array.isArray(groups)) continue;
1803
+ if (!merged[event]) merged[event] = [];
1804
+ merged[event].push(...groups);
1805
+ }
1806
+ }
1807
+ return merged;
1808
+ }
1274
1809
  function App(props) {
1275
1810
  const { exit } = useApp();
1276
- const { session, permissionRequest, streamingText, clearStreamingText, activeTools } = useSession(props);
1811
+ const { registry, pluginHooks } = useCommandRegistry(props.cwd ?? process.cwd());
1812
+ const configWithPluginHooks = {
1813
+ ...props.config,
1814
+ hooks: mergeHooksIntoConfig(
1815
+ props.config.hooks,
1816
+ pluginHooks
1817
+ )
1818
+ };
1819
+ const { session, permissionRequest, streamingText, clearStreamingText, activeTools } = useSession(
1820
+ { ...props, config: configWithPluginHooks }
1821
+ );
1277
1822
  const { messages, setMessages, addMessage } = useMessages();
1278
1823
  const [isThinking, setIsThinking] = useState7(false);
1279
1824
  const initialCtx = session.getContextState();
@@ -1282,9 +1827,9 @@ function App(props) {
1282
1827
  usedTokens: initialCtx.usedTokens,
1283
1828
  maxTokens: initialCtx.maxTokens
1284
1829
  });
1285
- const registry = useCommandRegistry(props.cwd ?? process.cwd());
1286
1830
  const pendingModelChangeRef = useRef5(null);
1287
1831
  const [pendingModelId, setPendingModelId] = useState7(null);
1832
+ const pluginCallbacks = usePluginCallbacks(props.cwd ?? process.cwd());
1288
1833
  const handleSlashCommand = useSlashCommands(
1289
1834
  session,
1290
1835
  addMessage,
@@ -1292,7 +1837,8 @@ function App(props) {
1292
1837
  exit,
1293
1838
  registry,
1294
1839
  pendingModelChangeRef,
1295
- setPendingModelId
1840
+ setPendingModelId,
1841
+ pluginCallbacks
1296
1842
  );
1297
1843
  const handleSubmit = useSubmitHandler(
1298
1844
  session,
@@ -1407,23 +1953,24 @@ function renderApp(options) {
1407
1953
  }
1408
1954
 
1409
1955
  // src/cli.ts
1410
- function hasValidSettingsFile(filePath) {
1411
- if (!existsSync3(filePath)) return false;
1956
+ function checkSettingsFile(filePath) {
1957
+ if (!existsSync3(filePath)) return "missing";
1412
1958
  try {
1413
1959
  const raw = readFileSync3(filePath, "utf8").trim();
1414
- if (raw.length === 0) return false;
1960
+ if (raw.length === 0) return "incomplete";
1415
1961
  const parsed = JSON.parse(raw);
1416
1962
  const provider = parsed.provider;
1417
- return !!provider?.apiKey;
1963
+ if (!provider?.apiKey) return "incomplete";
1964
+ return "valid";
1418
1965
  } catch {
1419
- return false;
1966
+ return "corrupt";
1420
1967
  }
1421
1968
  }
1422
1969
  function readVersion() {
1423
1970
  try {
1424
1971
  const thisFile = fileURLToPath(import.meta.url);
1425
- const dir = dirname2(thisFile);
1426
- const candidates = [join3(dir, "..", "..", "package.json"), join3(dir, "..", "package.json")];
1972
+ const dir = dirname3(thisFile);
1973
+ const candidates = [join5(dir, "..", "..", "package.json"), join5(dir, "..", "package.json")];
1427
1974
  for (const pkgPath of candidates) {
1428
1975
  try {
1429
1976
  const raw = readFileSync3(pkgPath, "utf-8");
@@ -1476,14 +2023,36 @@ function promptInput(label, masked = false) {
1476
2023
  }
1477
2024
  async function ensureConfig(cwd) {
1478
2025
  const userPath = getUserSettingsPath();
1479
- const projectPath = join3(cwd, ".robota", "settings.json");
1480
- const localPath = join3(cwd, ".robota", "settings.local.json");
1481
- if (hasValidSettingsFile(userPath) || hasValidSettingsFile(projectPath) || hasValidSettingsFile(localPath)) {
2026
+ const projectPath = join5(cwd, ".robota", "settings.json");
2027
+ const localPath = join5(cwd, ".robota", "settings.local.json");
2028
+ const paths = [userPath, projectPath, localPath];
2029
+ const checks = paths.map((p) => ({ path: p, status: checkSettingsFile(p) }));
2030
+ if (checks.some((c) => c.status === "valid")) {
1482
2031
  return;
1483
2032
  }
2033
+ const corrupt = checks.filter((c) => c.status === "corrupt");
2034
+ const incomplete = checks.filter((c) => c.status === "incomplete");
1484
2035
  process.stdout.write("\n");
1485
- process.stdout.write(" Welcome to Robota CLI!\n");
1486
- process.stdout.write(" No configuration found. Let's set up.\n");
2036
+ if (corrupt.length > 0) {
2037
+ for (const c of corrupt) {
2038
+ process.stderr.write(` ERROR: Settings file is corrupt (invalid JSON): ${c.path}
2039
+ `);
2040
+ }
2041
+ process.stdout.write("\n");
2042
+ }
2043
+ if (incomplete.length > 0) {
2044
+ for (const c of incomplete) {
2045
+ process.stderr.write(` WARNING: Settings file is missing provider.apiKey: ${c.path}
2046
+ `);
2047
+ }
2048
+ process.stdout.write("\n");
2049
+ }
2050
+ if (corrupt.length === 0 && incomplete.length === 0) {
2051
+ process.stdout.write(" Welcome to Robota CLI!\n");
2052
+ process.stdout.write(" No configuration found. Let's set up.\n");
2053
+ } else {
2054
+ process.stdout.write(" Reconfiguring...\n");
2055
+ }
1487
2056
  process.stdout.write("\n");
1488
2057
  const apiKey = await promptInput(" Anthropic API key: ", true);
1489
2058
  if (!apiKey) {
@@ -1491,7 +2060,7 @@ async function ensureConfig(cwd) {
1491
2060
  process.exit(1);
1492
2061
  }
1493
2062
  const language = await promptInput(" Response language (ko/en/ja/zh, default: en): ");
1494
- const settingsDir = dirname2(userPath);
2063
+ const settingsDir = dirname3(userPath);
1495
2064
  mkdirSync2(settingsDir, { recursive: true });
1496
2065
  const settings = {
1497
2066
  provider: {