@robota-sdk/agent-cli 3.0.0-beta.23 → 3.0.0-beta.25

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,
@@ -369,7 +369,122 @@ function handleReset(addMessage) {
369
369
  }
370
370
  return { handled: true, exitRequested: true };
371
371
  }
372
- async function executeSlashCommand(cmd, args, session, addMessage, clearMessages, registry) {
372
+ async function handlePluginCommand(args, addMessage, callbacks) {
373
+ const parts = args.trim().split(/\s+/);
374
+ const subcommand = parts[0] ?? "";
375
+ const subArgs = parts.slice(1).join(" ").trim();
376
+ try {
377
+ switch (subcommand) {
378
+ case "":
379
+ case void 0: {
380
+ const plugins = await callbacks.listInstalled();
381
+ if (plugins.length === 0) {
382
+ addMessage({ role: "system", content: "No plugins installed." });
383
+ } else {
384
+ const lines = plugins.map(
385
+ (p) => ` ${p.name} \u2014 ${p.description} [${p.enabled ? "enabled" : "disabled"}]`
386
+ );
387
+ addMessage({ role: "system", content: `Installed plugins:
388
+ ${lines.join("\n")}` });
389
+ }
390
+ return { handled: true };
391
+ }
392
+ case "install": {
393
+ if (!subArgs) {
394
+ addMessage({ role: "system", content: "Usage: /plugin install <name>@<marketplace>" });
395
+ return { handled: true };
396
+ }
397
+ await callbacks.install(subArgs);
398
+ addMessage({ role: "system", content: `Installed plugin: ${subArgs}` });
399
+ return { handled: true };
400
+ }
401
+ case "uninstall": {
402
+ if (!subArgs) {
403
+ addMessage({ role: "system", content: "Usage: /plugin uninstall <name>@<marketplace>" });
404
+ return { handled: true };
405
+ }
406
+ await callbacks.uninstall(subArgs);
407
+ addMessage({ role: "system", content: `Uninstalled plugin: ${subArgs}` });
408
+ return { handled: true };
409
+ }
410
+ case "enable": {
411
+ if (!subArgs) {
412
+ addMessage({ role: "system", content: "Usage: /plugin enable <name>@<marketplace>" });
413
+ return { handled: true };
414
+ }
415
+ await callbacks.enable(subArgs);
416
+ addMessage({ role: "system", content: `Enabled plugin: ${subArgs}` });
417
+ return { handled: true };
418
+ }
419
+ case "disable": {
420
+ if (!subArgs) {
421
+ addMessage({ role: "system", content: "Usage: /plugin disable <name>@<marketplace>" });
422
+ return { handled: true };
423
+ }
424
+ await callbacks.disable(subArgs);
425
+ addMessage({ role: "system", content: `Disabled plugin: ${subArgs}` });
426
+ return { handled: true };
427
+ }
428
+ case "marketplace": {
429
+ const mpParts = subArgs.split(/\s+/);
430
+ const mpSubcommand = mpParts[0] ?? "";
431
+ const mpArgs = mpParts.slice(1).join(" ").trim();
432
+ if (mpSubcommand === "add" && mpArgs) {
433
+ const registeredName = await callbacks.marketplaceAdd(mpArgs);
434
+ addMessage({
435
+ role: "system",
436
+ content: `Added marketplace: "${registeredName}" (from ${mpArgs})
437
+ Install plugins with: /plugin install <name>@${registeredName}`
438
+ });
439
+ return { handled: true };
440
+ } else if (mpSubcommand === "remove" && mpArgs) {
441
+ await callbacks.marketplaceRemove(mpArgs);
442
+ addMessage({
443
+ role: "system",
444
+ content: `Removed marketplace "${mpArgs}" and uninstalled its plugins.`
445
+ });
446
+ return { handled: true };
447
+ } else if (mpSubcommand === "update" && mpArgs) {
448
+ await callbacks.marketplaceUpdate(mpArgs);
449
+ addMessage({
450
+ role: "system",
451
+ content: `Updated marketplace "${mpArgs}".`
452
+ });
453
+ return { handled: true };
454
+ } else if (mpSubcommand === "list") {
455
+ const sources = await callbacks.marketplaceList();
456
+ if (sources.length === 0) {
457
+ addMessage({ role: "system", content: "No marketplace sources configured." });
458
+ } else {
459
+ const lines = sources.map((s) => ` ${s.name} (${s.type})`);
460
+ addMessage({ role: "system", content: `Marketplace sources:
461
+ ${lines.join("\n")}` });
462
+ }
463
+ return { handled: true };
464
+ } else {
465
+ addMessage({
466
+ role: "system",
467
+ content: "Usage: /plugin marketplace add <source> | remove <name> | update <name> | list"
468
+ });
469
+ return { handled: true };
470
+ }
471
+ }
472
+ default:
473
+ addMessage({ role: "system", content: `Unknown plugin subcommand: ${subcommand}` });
474
+ return { handled: true };
475
+ }
476
+ } catch (error) {
477
+ const message = error instanceof Error ? error.message : String(error);
478
+ addMessage({ role: "system", content: `Plugin error: ${message}` });
479
+ return { handled: true };
480
+ }
481
+ }
482
+ async function handleReloadPlugins(addMessage, callbacks) {
483
+ await callbacks.reloadPlugins();
484
+ addMessage({ role: "system", content: "Plugins reload complete." });
485
+ return { handled: true };
486
+ }
487
+ async function executeSlashCommand(cmd, args, session, addMessage, clearMessages, registry, pluginCallbacks) {
373
488
  switch (cmd) {
374
489
  case "help":
375
490
  return handleHelp(addMessage);
@@ -393,10 +508,22 @@ async function executeSlashCommand(cmd, args, session, addMessage, clearMessages
393
508
  return handleReset(addMessage);
394
509
  case "exit":
395
510
  return { handled: true, exitRequested: true };
511
+ case "plugin":
512
+ if (pluginCallbacks) {
513
+ return handlePluginCommand(args, addMessage, pluginCallbacks);
514
+ }
515
+ addMessage({ role: "system", content: "Plugin management is not available." });
516
+ return { handled: true };
517
+ case "reload-plugins":
518
+ if (pluginCallbacks) {
519
+ return handleReloadPlugins(addMessage, pluginCallbacks);
520
+ }
521
+ addMessage({ role: "system", content: "Plugin management is not available." });
522
+ return { handled: true };
396
523
  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}` });
524
+ const dynamicCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
525
+ if (dynamicCmd) {
526
+ addMessage({ role: "system", content: `Invoking ${dynamicCmd.source}: ${cmd}` });
400
527
  return { handled: false };
401
528
  }
402
529
  addMessage({ role: "system", content: `Unknown command "/${cmd}". Type /help for help.` });
@@ -407,14 +534,22 @@ async function executeSlashCommand(cmd, args, session, addMessage, clearMessages
407
534
 
408
535
  // src/ui/hooks/useSlashCommands.ts
409
536
  var EXIT_DELAY_MS = 500;
410
- function useSlashCommands(session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId) {
537
+ function useSlashCommands(session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId, pluginCallbacks) {
411
538
  return useCallback3(
412
539
  async (input) => {
413
540
  const parts = input.slice(1).split(/\s+/);
414
541
  const cmd = parts[0]?.toLowerCase() ?? "";
415
542
  const args = parts.slice(1).join(" ");
416
543
  const clearMessages = () => setMessages([]);
417
- const result = await executeSlashCommand(cmd, args, session, addMessage, clearMessages, registry);
544
+ const result = await executeSlashCommand(
545
+ cmd,
546
+ args,
547
+ session,
548
+ addMessage,
549
+ clearMessages,
550
+ registry,
551
+ pluginCallbacks
552
+ );
418
553
  if (result.pendingModelId) {
419
554
  pendingModelChangeRef.current = result.pendingModelId;
420
555
  setPendingModelId(result.pendingModelId);
@@ -424,7 +559,16 @@ function useSlashCommands(session, addMessage, setMessages, exit, registry, pend
424
559
  }
425
560
  return result.handled;
426
561
  },
427
- [session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId]
562
+ [
563
+ session,
564
+ addMessage,
565
+ setMessages,
566
+ exit,
567
+ registry,
568
+ pendingModelChangeRef,
569
+ setPendingModelId,
570
+ pluginCallbacks
571
+ ]
428
572
  );
429
573
  }
430
574
 
@@ -459,16 +603,60 @@ function parseFirstArgValue(argsJson) {
459
603
  }
460
604
 
461
605
  // src/utils/skill-prompt.ts
462
- function buildSkillPrompt(input, registry) {
606
+ import { execSync } from "child_process";
607
+ function substituteVariables(content, args, context) {
608
+ const argParts = args ? args.split(/\s+/) : [];
609
+ let result = content;
610
+ result = result.replace(/\$ARGUMENTS\[(\d+)]/g, (_match, index) => {
611
+ return argParts[Number(index)] ?? "";
612
+ });
613
+ result = result.replace(/\$ARGUMENTS/g, args);
614
+ result = result.replace(/\$(\d)(?!\d|\w|\[)/g, (_match, digit) => {
615
+ return argParts[Number(digit)] ?? "";
616
+ });
617
+ result = result.replace(/\$\{CLAUDE_SESSION_ID}/g, context?.sessionId ?? "");
618
+ result = result.replace(/\$\{CLAUDE_SKILL_DIR}/g, context?.skillDir ?? "");
619
+ return result;
620
+ }
621
+ async function preprocessShellCommands(content) {
622
+ const shellPattern = /!`([^`]+)`/g;
623
+ if (!shellPattern.test(content)) {
624
+ return content;
625
+ }
626
+ shellPattern.lastIndex = 0;
627
+ let result = content;
628
+ let match;
629
+ const matches = [];
630
+ while ((match = shellPattern.exec(content)) !== null) {
631
+ matches.push({ full: match[0], command: match[1] });
632
+ }
633
+ for (const { full, command } of matches) {
634
+ let output = "";
635
+ try {
636
+ output = execSync(command, {
637
+ timeout: 5e3,
638
+ encoding: "utf-8",
639
+ stdio: ["pipe", "pipe", "pipe"]
640
+ }).trimEnd();
641
+ } catch {
642
+ output = "";
643
+ }
644
+ result = result.replace(full, output);
645
+ }
646
+ return result;
647
+ }
648
+ async function buildSkillPrompt(input, registry, context) {
463
649
  const parts = input.slice(1).split(/\s+/);
464
650
  const cmd = parts[0]?.toLowerCase() ?? "";
465
- const skillCmd = registry.getCommands().find((c) => c.name === cmd && c.source === "skill");
651
+ const skillCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
466
652
  if (!skillCmd) return null;
467
653
  const args = parts.slice(1).join(" ").trim();
468
654
  const userInstruction = args || skillCmd.description;
469
655
  if (skillCmd.skillContent) {
656
+ let processed = await preprocessShellCommands(skillCmd.skillContent);
657
+ processed = substituteVariables(processed, args, context);
470
658
  return `<skill name="${cmd}">
471
- ${skillCmd.skillContent}
659
+ ${processed}
472
660
  </skill>
473
661
 
474
662
  Execute the "${cmd}" skill: ${userInstruction}`;
@@ -481,12 +669,12 @@ function syncContextState(session, setter) {
481
669
  const ctx = session.getContextState();
482
670
  setter({ percentage: ctx.usedPercentage, usedTokens: ctx.usedTokens, maxTokens: ctx.maxTokens });
483
671
  }
484
- async function runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextState) {
672
+ async function runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextState, rawInput) {
485
673
  setIsThinking(true);
486
674
  clearStreamingText();
487
675
  const historyBefore = session.getHistory().length;
488
676
  try {
489
- const response = await session.run(prompt);
677
+ const response = await session.run(prompt, rawInput);
490
678
  clearStreamingText();
491
679
  const history = session.getHistory();
492
680
  const toolLines = extractToolCalls(
@@ -494,7 +682,11 @@ async function runSessionPrompt(prompt, session, addMessage, clearStreamingText,
494
682
  historyBefore
495
683
  );
496
684
  if (toolLines.length > 0) {
497
- addMessage({ role: "tool", content: toolLines.join("\n"), toolName: `${toolLines.length} tools` });
685
+ addMessage({
686
+ role: "tool",
687
+ content: toolLines.join("\n"),
688
+ toolName: `${toolLines.length} tools`
689
+ });
498
690
  }
499
691
  addMessage({ role: "assistant", content: response || "(empty response)" });
500
692
  syncContextState(session, setContextState);
@@ -519,19 +711,45 @@ function useSubmitHandler(session, addMessage, handleSlashCommand, clearStreamin
519
711
  syncContextState(session, setContextState);
520
712
  return;
521
713
  }
522
- const prompt = buildSkillPrompt(input, registry);
714
+ const prompt = await buildSkillPrompt(input, registry);
523
715
  if (!prompt) return;
524
- return runSessionPrompt(prompt, session, addMessage, clearStreamingText, setIsThinking, setContextState);
716
+ return runSessionPrompt(
717
+ prompt,
718
+ session,
719
+ addMessage,
720
+ clearStreamingText,
721
+ setIsThinking,
722
+ setContextState,
723
+ input
724
+ );
525
725
  }
526
726
  addMessage({ role: "user", content: input });
527
- return runSessionPrompt(input, session, addMessage, clearStreamingText, setIsThinking, setContextState);
727
+ return runSessionPrompt(
728
+ input,
729
+ session,
730
+ addMessage,
731
+ clearStreamingText,
732
+ setIsThinking,
733
+ setContextState
734
+ );
528
735
  },
529
- [session, addMessage, handleSlashCommand, clearStreamingText, setIsThinking, setContextState, registry]
736
+ [
737
+ session,
738
+ addMessage,
739
+ handleSlashCommand,
740
+ clearStreamingText,
741
+ setIsThinking,
742
+ setContextState,
743
+ registry
744
+ ]
530
745
  );
531
746
  }
532
747
 
533
748
  // src/ui/hooks/useCommandRegistry.ts
534
749
  import { useRef as useRef2 } from "react";
750
+ import { homedir as homedir2 } from "os";
751
+ import { join as join3, dirname as dirname2 } from "path";
752
+ import { BundlePluginLoader } from "@robota-sdk/agent-sdk";
535
753
 
536
754
  // src/commands/command-registry.ts
537
755
  var CommandRegistry = class {
@@ -615,6 +833,23 @@ function createBuiltinCommands() {
615
833
  { name: "cost", description: "Show session info", source: "builtin" },
616
834
  { name: "context", description: "Context window info", source: "builtin" },
617
835
  { name: "permissions", description: "Permission rules", source: "builtin" },
836
+ {
837
+ name: "plugin",
838
+ description: "Manage plugins",
839
+ source: "builtin",
840
+ subcommands: [
841
+ { name: "install", description: "Install a plugin (name@marketplace)", source: "builtin" },
842
+ {
843
+ name: "uninstall",
844
+ description: "Uninstall a plugin (name@marketplace)",
845
+ source: "builtin"
846
+ },
847
+ { name: "enable", description: "Enable a plugin (name@marketplace)", source: "builtin" },
848
+ { name: "disable", description: "Disable a plugin (name@marketplace)", source: "builtin" },
849
+ { name: "marketplace", description: "Manage marketplace sources", source: "builtin" }
850
+ ]
851
+ },
852
+ { name: "reload-plugins", description: "Reload all plugin resources", source: "builtin" },
618
853
  { name: "reset", description: "Delete settings and exit", source: "builtin" },
619
854
  { name: "exit", description: "Exit CLI", source: "builtin" }
620
855
  ];
@@ -632,27 +867,52 @@ var BuiltinCommandSource = class {
632
867
 
633
868
  // src/commands/skill-source.ts
634
869
  import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
635
- import { join as join2 } from "path";
870
+ import { join as join2, basename } from "path";
636
871
  import { homedir } from "os";
872
+ var BOOLEAN_KEYS = /* @__PURE__ */ new Set(["disable-model-invocation", "user-invocable"]);
873
+ var LIST_KEYS = /* @__PURE__ */ new Set(["allowed-tools"]);
874
+ function kebabToCamel(key) {
875
+ return key.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
876
+ }
637
877
  function parseFrontmatter(content) {
638
878
  const lines = content.split("\n");
639
879
  if (lines[0]?.trim() !== "---") return null;
640
- let name = "";
641
- let description = "";
880
+ const result = {};
642
881
  for (let i = 1; i < lines.length; i++) {
643
882
  const line = lines[i];
644
883
  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();
884
+ const match = line.match(/^([a-z][a-z0-9-]*):\s*(.+)/);
885
+ if (!match) continue;
886
+ const key = match[1];
887
+ const rawValue = match[2].trim();
888
+ const camelKey = kebabToCamel(key);
889
+ if (BOOLEAN_KEYS.has(key)) {
890
+ result[camelKey] = rawValue === "true";
891
+ } else if (LIST_KEYS.has(key)) {
892
+ result[camelKey] = rawValue.split(",").map((s) => s.trim());
893
+ } else {
894
+ result[camelKey] = rawValue;
653
895
  }
654
896
  }
655
- return name ? { name, description } : null;
897
+ return Object.keys(result).length > 0 ? result : null;
898
+ }
899
+ function buildCommand(frontmatter, content, fallbackName) {
900
+ const cmd = {
901
+ name: frontmatter?.name ?? fallbackName,
902
+ description: frontmatter?.description ?? `Skill: ${fallbackName}`,
903
+ source: "skill",
904
+ skillContent: content
905
+ };
906
+ if (frontmatter?.argumentHint !== void 0) cmd.argumentHint = frontmatter.argumentHint;
907
+ if (frontmatter?.disableModelInvocation !== void 0)
908
+ cmd.disableModelInvocation = frontmatter.disableModelInvocation;
909
+ if (frontmatter?.userInvocable !== void 0) cmd.userInvocable = frontmatter.userInvocable;
910
+ if (frontmatter?.allowedTools !== void 0) cmd.allowedTools = frontmatter.allowedTools;
911
+ if (frontmatter?.model !== void 0) cmd.model = frontmatter.model;
912
+ if (frontmatter?.effort !== void 0) cmd.effort = frontmatter.effort;
913
+ if (frontmatter?.context !== void 0) cmd.context = frontmatter.context;
914
+ if (frontmatter?.agent !== void 0) cmd.agent = frontmatter.agent;
915
+ return cmd;
656
916
  }
657
917
  function scanSkillsDir(skillsDir) {
658
918
  if (!existsSync2(skillsDir)) return [];
@@ -664,48 +924,251 @@ function scanSkillsDir(skillsDir) {
664
924
  if (!existsSync2(skillFile)) continue;
665
925
  const content = readFileSync2(skillFile, "utf-8");
666
926
  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
- });
927
+ commands.push(buildCommand(frontmatter, content, entry.name));
928
+ }
929
+ return commands;
930
+ }
931
+ function scanCommandsDir(commandsDir) {
932
+ if (!existsSync2(commandsDir)) return [];
933
+ const commands = [];
934
+ const entries = readdirSync(commandsDir, { withFileTypes: true });
935
+ for (const entry of entries) {
936
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
937
+ const filePath = join2(commandsDir, entry.name);
938
+ const content = readFileSync2(filePath, "utf-8");
939
+ const frontmatter = parseFrontmatter(content);
940
+ const fallbackName = basename(entry.name, ".md");
941
+ commands.push(buildCommand(frontmatter, content, fallbackName));
673
942
  }
674
943
  return commands;
675
944
  }
676
945
  var SkillCommandSource = class {
677
946
  name = "skill";
678
947
  cwd;
948
+ home;
679
949
  cachedCommands = null;
680
- constructor(cwd) {
950
+ constructor(cwd, home) {
681
951
  this.cwd = cwd;
952
+ this.home = home ?? homedir();
682
953
  }
683
954
  getCommands() {
684
955
  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);
956
+ const sources = [
957
+ scanSkillsDir(join2(this.cwd, ".claude", "skills")),
958
+ // 1. project .claude/skills
959
+ scanCommandsDir(join2(this.cwd, ".claude", "commands")),
960
+ // 2. project .claude/commands (legacy)
961
+ scanSkillsDir(join2(this.home, ".robota", "skills")),
962
+ // 3. user ~/.robota/skills
963
+ scanSkillsDir(join2(this.cwd, ".agents", "skills"))
964
+ // 4. project .agents/skills
965
+ ];
966
+ const seen = /* @__PURE__ */ new Set();
967
+ const merged = [];
968
+ for (const commands of sources) {
969
+ for (const cmd of commands) {
970
+ if (!seen.has(cmd.name)) {
971
+ seen.add(cmd.name);
972
+ merged.push(cmd);
973
+ }
692
974
  }
693
975
  }
694
976
  this.cachedCommands = merged;
695
977
  return this.cachedCommands;
696
978
  }
979
+ /** Get skills that models can invoke (excludes disableModelInvocation: true) */
980
+ getModelInvocableSkills() {
981
+ return this.getCommands().filter((cmd) => cmd.disableModelInvocation !== true);
982
+ }
983
+ /** Get skills that users can invoke (excludes userInvocable: false) */
984
+ getUserInvocableSkills() {
985
+ return this.getCommands().filter((cmd) => cmd.userInvocable !== false);
986
+ }
987
+ };
988
+
989
+ // src/commands/plugin-source.ts
990
+ var PluginCommandSource = class {
991
+ name = "plugin";
992
+ plugins;
993
+ constructor(plugins) {
994
+ this.plugins = plugins;
995
+ }
996
+ getCommands() {
997
+ const commands = [];
998
+ for (const plugin of this.plugins) {
999
+ for (const skill of plugin.skills) {
1000
+ const baseName = skill.name.includes("@") ? skill.name.split("@")[0] : skill.name;
1001
+ commands.push({
1002
+ name: baseName,
1003
+ description: `${skill.description} (${plugin.manifest.name})`,
1004
+ source: "plugin",
1005
+ skillContent: skill.skillContent,
1006
+ pluginDir: plugin.pluginDir
1007
+ });
1008
+ }
1009
+ for (const cmd of plugin.commands) {
1010
+ commands.push({
1011
+ name: cmd.name,
1012
+ description: cmd.description,
1013
+ source: "plugin",
1014
+ skillContent: cmd.skillContent,
1015
+ pluginDir: plugin.pluginDir
1016
+ });
1017
+ }
1018
+ }
1019
+ return commands;
1020
+ }
697
1021
  };
698
1022
 
699
1023
  // src/ui/hooks/useCommandRegistry.ts
1024
+ function buildPluginEnv(plugin) {
1025
+ const dataDir = join3(dirname2(dirname2(plugin.pluginDir)), "data", plugin.manifest.name);
1026
+ return {
1027
+ CLAUDE_PLUGIN_ROOT: plugin.pluginDir,
1028
+ CLAUDE_PLUGIN_PATH: plugin.pluginDir,
1029
+ CLAUDE_PLUGIN_DATA: dataDir
1030
+ };
1031
+ }
1032
+ function mergePluginHooks(plugins) {
1033
+ const merged = {};
1034
+ for (const plugin of plugins) {
1035
+ const hooksObj = plugin.hooks;
1036
+ if (!hooksObj) continue;
1037
+ const pluginEnv = buildPluginEnv(plugin);
1038
+ const innerHooks = hooksObj.hooks ?? hooksObj;
1039
+ for (const [event, groups] of Object.entries(innerHooks)) {
1040
+ if (!Array.isArray(groups)) continue;
1041
+ if (!merged[event]) merged[event] = [];
1042
+ const resolved = groups.map((group) => {
1043
+ const resolved2 = resolvePluginRoot(group, plugin.pluginDir);
1044
+ if (typeof resolved2 === "object" && resolved2 !== null) {
1045
+ resolved2.env = pluginEnv;
1046
+ }
1047
+ return resolved2;
1048
+ });
1049
+ merged[event].push(...resolved);
1050
+ }
1051
+ }
1052
+ return merged;
1053
+ }
1054
+ function resolvePluginRoot(group, pluginDir) {
1055
+ if (typeof group !== "object" || group === null) return group;
1056
+ const obj = group;
1057
+ if (Array.isArray(obj.hooks)) {
1058
+ return {
1059
+ ...obj,
1060
+ hooks: obj.hooks.map((h) => {
1061
+ if (typeof h !== "object" || h === null) return h;
1062
+ const hook = h;
1063
+ if (typeof hook.command === "string") {
1064
+ return {
1065
+ ...hook,
1066
+ command: hook.command.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pluginDir)
1067
+ };
1068
+ }
1069
+ return hook;
1070
+ })
1071
+ };
1072
+ }
1073
+ return group;
1074
+ }
700
1075
  function useCommandRegistry(cwd) {
701
- const registryRef = useRef2(null);
702
- if (registryRef.current === null) {
1076
+ const resultRef = useRef2(null);
1077
+ if (resultRef.current === null) {
703
1078
  const registry = new CommandRegistry();
704
1079
  registry.addSource(new BuiltinCommandSource());
705
1080
  registry.addSource(new SkillCommandSource(cwd));
706
- registryRef.current = registry;
1081
+ let pluginHooks = {};
1082
+ const pluginsDir = join3(homedir2(), ".robota", "plugins");
1083
+ const loader = new BundlePluginLoader(pluginsDir);
1084
+ try {
1085
+ const plugins = loader.loadPluginsSync();
1086
+ if (plugins.length > 0) {
1087
+ registry.addSource(new PluginCommandSource(plugins));
1088
+ pluginHooks = mergePluginHooks(plugins);
1089
+ }
1090
+ } catch {
1091
+ }
1092
+ resultRef.current = { registry, pluginHooks };
707
1093
  }
708
- return registryRef.current;
1094
+ return resultRef.current;
1095
+ }
1096
+
1097
+ // src/ui/hooks/usePluginCallbacks.ts
1098
+ import { useMemo } from "react";
1099
+ import { homedir as homedir3 } from "os";
1100
+ import { join as join4 } from "path";
1101
+ import {
1102
+ PluginSettingsStore,
1103
+ BundlePluginLoader as BundlePluginLoader2,
1104
+ BundlePluginInstaller,
1105
+ MarketplaceClient
1106
+ } from "@robota-sdk/agent-sdk";
1107
+ function usePluginCallbacks(cwd) {
1108
+ return useMemo(() => {
1109
+ const home = homedir3();
1110
+ const pluginsDir = join4(home, ".robota", "plugins");
1111
+ const userSettingsPath = join4(home, ".robota", "settings.json");
1112
+ const settingsStore = new PluginSettingsStore(userSettingsPath);
1113
+ const marketplace = new MarketplaceClient({ pluginsDir });
1114
+ const installer = new BundlePluginInstaller({
1115
+ pluginsDir,
1116
+ settingsStore,
1117
+ marketplaceClient: marketplace
1118
+ });
1119
+ const loader = new BundlePluginLoader2(pluginsDir);
1120
+ return {
1121
+ listInstalled: async () => {
1122
+ const plugins = await loader.loadAll();
1123
+ return plugins.map((p) => ({
1124
+ name: p.manifest.name,
1125
+ description: p.manifest.description,
1126
+ enabled: true
1127
+ }));
1128
+ },
1129
+ install: async (pluginId) => {
1130
+ const [name, marketplaceName] = pluginId.split("@");
1131
+ if (!name || !marketplaceName) {
1132
+ throw new Error("Plugin ID must be in format: name@marketplace");
1133
+ }
1134
+ await installer.install(name, marketplaceName);
1135
+ },
1136
+ uninstall: async (pluginId) => {
1137
+ await installer.uninstall(pluginId);
1138
+ },
1139
+ enable: async (pluginId) => {
1140
+ await installer.enable(pluginId);
1141
+ },
1142
+ disable: async (pluginId) => {
1143
+ await installer.disable(pluginId);
1144
+ },
1145
+ marketplaceAdd: async (source) => {
1146
+ if (source.includes("/") && !source.includes(":")) {
1147
+ return marketplace.addMarketplace({ type: "github", repo: source });
1148
+ } else {
1149
+ return marketplace.addMarketplace({ type: "git", url: source });
1150
+ }
1151
+ },
1152
+ marketplaceRemove: async (name) => {
1153
+ const installedFromMarketplace = installer.getPluginsByMarketplace(name);
1154
+ for (const record of installedFromMarketplace) {
1155
+ await installer.uninstall(`${record.pluginName}@${record.marketplace}`);
1156
+ }
1157
+ marketplace.removeMarketplace(name);
1158
+ },
1159
+ marketplaceUpdate: async (name) => {
1160
+ marketplace.updateMarketplace(name);
1161
+ },
1162
+ marketplaceList: async () => {
1163
+ return marketplace.listMarketplaces().map((m) => ({
1164
+ name: m.name,
1165
+ type: m.source.type
1166
+ }));
1167
+ },
1168
+ reloadPlugins: async () => {
1169
+ }
1170
+ };
1171
+ }, [cwd]);
709
1172
  }
710
1173
 
711
1174
  // src/ui/MessageList.tsx
@@ -828,7 +1291,7 @@ function StatusBar({
828
1291
  }
829
1292
 
830
1293
  // src/ui/InputArea.tsx
831
- import React3, { useState as useState5, useCallback as useCallback5, useMemo } from "react";
1294
+ import React4, { useState as useState5, useCallback as useCallback5, useMemo as useMemo2 } from "react";
832
1295
  import { Box as Box4, Text as Text6, useInput as useInput2 } from "ink";
833
1296
 
834
1297
  // src/ui/CjkTextInput.tsx
@@ -1007,14 +1470,14 @@ function parseSlashInput(value) {
1007
1470
  function useAutocomplete(value, registry) {
1008
1471
  const [selectedIndex, setSelectedIndex] = useState5(0);
1009
1472
  const [dismissed, setDismissed] = useState5(false);
1010
- const prevValueRef = React3.useRef(value);
1473
+ const prevValueRef = React4.useRef(value);
1011
1474
  if (prevValueRef.current !== value) {
1012
1475
  prevValueRef.current = value;
1013
1476
  if (dismissed) setDismissed(false);
1014
1477
  }
1015
1478
  const parsed = parseSlashInput(value);
1016
1479
  const isSubcommandMode = parsed.isSlash && parsed.parentCommand.length > 0;
1017
- const filteredCommands = useMemo(() => {
1480
+ const filteredCommands = useMemo2(() => {
1018
1481
  if (!registry || !parsed.isSlash || dismissed) return [];
1019
1482
  if (isSubcommandMode) {
1020
1483
  const subs = registry.getSubcommands(parsed.parentCommand);
@@ -1174,7 +1637,7 @@ function ConfirmPrompt({
1174
1637
  }
1175
1638
 
1176
1639
  // src/ui/PermissionPrompt.tsx
1177
- import React5 from "react";
1640
+ import React6 from "react";
1178
1641
  import { Box as Box6, Text as Text8, useInput as useInput4 } from "ink";
1179
1642
  import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1180
1643
  var OPTIONS = ["Allow", "Allow always (this session)", "Deny"];
@@ -1184,15 +1647,15 @@ function formatArgs(args) {
1184
1647
  return entries.map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`).join(", ");
1185
1648
  }
1186
1649
  function PermissionPrompt({ request }) {
1187
- const [selected, setSelected] = React5.useState(0);
1188
- const resolvedRef = React5.useRef(false);
1189
- const prevRequestRef = React5.useRef(request);
1650
+ const [selected, setSelected] = React6.useState(0);
1651
+ const resolvedRef = React6.useRef(false);
1652
+ const prevRequestRef = React6.useRef(request);
1190
1653
  if (prevRequestRef.current !== request) {
1191
1654
  prevRequestRef.current = request;
1192
1655
  resolvedRef.current = false;
1193
1656
  setSelected(0);
1194
1657
  }
1195
- const doResolve = React5.useCallback(
1658
+ const doResolve = React6.useCallback(
1196
1659
  (index) => {
1197
1660
  if (resolvedRef.current) return;
1198
1661
  resolvedRef.current = true;
@@ -1271,9 +1734,35 @@ function StreamingIndicator({ text, activeTools }) {
1271
1734
  // src/ui/App.tsx
1272
1735
  import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1273
1736
  var EXIT_DELAY_MS2 = 500;
1737
+ function mergeHooksIntoConfig(configHooks, pluginHooks) {
1738
+ const pluginKeys = Object.keys(pluginHooks);
1739
+ if (pluginKeys.length === 0) return configHooks;
1740
+ const merged = {};
1741
+ for (const [event, groups] of Object.entries(pluginHooks)) {
1742
+ merged[event] = [...groups];
1743
+ }
1744
+ if (configHooks) {
1745
+ for (const [event, groups] of Object.entries(configHooks)) {
1746
+ if (!Array.isArray(groups)) continue;
1747
+ if (!merged[event]) merged[event] = [];
1748
+ merged[event].push(...groups);
1749
+ }
1750
+ }
1751
+ return merged;
1752
+ }
1274
1753
  function App(props) {
1275
1754
  const { exit } = useApp();
1276
- const { session, permissionRequest, streamingText, clearStreamingText, activeTools } = useSession(props);
1755
+ const { registry, pluginHooks } = useCommandRegistry(props.cwd ?? process.cwd());
1756
+ const configWithPluginHooks = {
1757
+ ...props.config,
1758
+ hooks: mergeHooksIntoConfig(
1759
+ props.config.hooks,
1760
+ pluginHooks
1761
+ )
1762
+ };
1763
+ const { session, permissionRequest, streamingText, clearStreamingText, activeTools } = useSession(
1764
+ { ...props, config: configWithPluginHooks }
1765
+ );
1277
1766
  const { messages, setMessages, addMessage } = useMessages();
1278
1767
  const [isThinking, setIsThinking] = useState7(false);
1279
1768
  const initialCtx = session.getContextState();
@@ -1282,10 +1771,19 @@ function App(props) {
1282
1771
  usedTokens: initialCtx.usedTokens,
1283
1772
  maxTokens: initialCtx.maxTokens
1284
1773
  });
1285
- const registry = useCommandRegistry(props.cwd ?? process.cwd());
1286
1774
  const pendingModelChangeRef = useRef5(null);
1287
1775
  const [pendingModelId, setPendingModelId] = useState7(null);
1288
- const handleSlashCommand = useSlashCommands(session, addMessage, setMessages, exit, registry, pendingModelChangeRef, setPendingModelId);
1776
+ const pluginCallbacks = usePluginCallbacks(props.cwd ?? process.cwd());
1777
+ const handleSlashCommand = useSlashCommands(
1778
+ session,
1779
+ addMessage,
1780
+ setMessages,
1781
+ exit,
1782
+ registry,
1783
+ pendingModelChangeRef,
1784
+ setPendingModelId,
1785
+ pluginCallbacks
1786
+ );
1289
1787
  const handleSubmit = useSubmitHandler(
1290
1788
  session,
1291
1789
  addMessage,
@@ -1332,10 +1830,16 @@ function App(props) {
1332
1830
  try {
1333
1831
  const settingsPath = getUserSettingsPath();
1334
1832
  updateModelInSettings(settingsPath, pendingModelId);
1335
- addMessage({ role: "system", content: `Model changed to ${getModelName(pendingModelId)}. Restarting...` });
1833
+ addMessage({
1834
+ role: "system",
1835
+ content: `Model changed to ${getModelName(pendingModelId)}. Restarting...`
1836
+ });
1336
1837
  setTimeout(() => exit(), EXIT_DELAY_MS2);
1337
1838
  } catch (err) {
1338
- addMessage({ role: "system", content: `Failed: ${err instanceof Error ? err.message : String(err)}` });
1839
+ addMessage({
1840
+ role: "system",
1841
+ content: `Failed: ${err instanceof Error ? err.message : String(err)}`
1842
+ });
1339
1843
  }
1340
1844
  } else {
1341
1845
  addMessage({ role: "system", content: "Model change cancelled." });
@@ -1393,23 +1897,24 @@ function renderApp(options) {
1393
1897
  }
1394
1898
 
1395
1899
  // src/cli.ts
1396
- function hasValidSettingsFile(filePath) {
1397
- if (!existsSync3(filePath)) return false;
1900
+ function checkSettingsFile(filePath) {
1901
+ if (!existsSync3(filePath)) return "missing";
1398
1902
  try {
1399
1903
  const raw = readFileSync3(filePath, "utf8").trim();
1400
- if (raw.length === 0) return false;
1904
+ if (raw.length === 0) return "incomplete";
1401
1905
  const parsed = JSON.parse(raw);
1402
1906
  const provider = parsed.provider;
1403
- return !!provider?.apiKey;
1907
+ if (!provider?.apiKey) return "incomplete";
1908
+ return "valid";
1404
1909
  } catch {
1405
- return false;
1910
+ return "corrupt";
1406
1911
  }
1407
1912
  }
1408
1913
  function readVersion() {
1409
1914
  try {
1410
1915
  const thisFile = fileURLToPath(import.meta.url);
1411
- const dir = dirname2(thisFile);
1412
- const candidates = [join3(dir, "..", "..", "package.json"), join3(dir, "..", "package.json")];
1916
+ const dir = dirname3(thisFile);
1917
+ const candidates = [join5(dir, "..", "..", "package.json"), join5(dir, "..", "package.json")];
1413
1918
  for (const pkgPath of candidates) {
1414
1919
  try {
1415
1920
  const raw = readFileSync3(pkgPath, "utf-8");
@@ -1462,14 +1967,36 @@ function promptInput(label, masked = false) {
1462
1967
  }
1463
1968
  async function ensureConfig(cwd) {
1464
1969
  const userPath = getUserSettingsPath();
1465
- const projectPath = join3(cwd, ".robota", "settings.json");
1466
- const localPath = join3(cwd, ".robota", "settings.local.json");
1467
- if (hasValidSettingsFile(userPath) || hasValidSettingsFile(projectPath) || hasValidSettingsFile(localPath)) {
1970
+ const projectPath = join5(cwd, ".robota", "settings.json");
1971
+ const localPath = join5(cwd, ".robota", "settings.local.json");
1972
+ const paths = [userPath, projectPath, localPath];
1973
+ const checks = paths.map((p) => ({ path: p, status: checkSettingsFile(p) }));
1974
+ if (checks.some((c) => c.status === "valid")) {
1468
1975
  return;
1469
1976
  }
1977
+ const corrupt = checks.filter((c) => c.status === "corrupt");
1978
+ const incomplete = checks.filter((c) => c.status === "incomplete");
1470
1979
  process.stdout.write("\n");
1471
- process.stdout.write(" Welcome to Robota CLI!\n");
1472
- process.stdout.write(" No configuration found. Let's set up.\n");
1980
+ if (corrupt.length > 0) {
1981
+ for (const c of corrupt) {
1982
+ process.stderr.write(` ERROR: Settings file is corrupt (invalid JSON): ${c.path}
1983
+ `);
1984
+ }
1985
+ process.stdout.write("\n");
1986
+ }
1987
+ if (incomplete.length > 0) {
1988
+ for (const c of incomplete) {
1989
+ process.stderr.write(` WARNING: Settings file is missing provider.apiKey: ${c.path}
1990
+ `);
1991
+ }
1992
+ process.stdout.write("\n");
1993
+ }
1994
+ if (corrupt.length === 0 && incomplete.length === 0) {
1995
+ process.stdout.write(" Welcome to Robota CLI!\n");
1996
+ process.stdout.write(" No configuration found. Let's set up.\n");
1997
+ } else {
1998
+ process.stdout.write(" Reconfiguring...\n");
1999
+ }
1473
2000
  process.stdout.write("\n");
1474
2001
  const apiKey = await promptInput(" Anthropic API key: ", true);
1475
2002
  if (!apiKey) {
@@ -1477,7 +2004,7 @@ async function ensureConfig(cwd) {
1477
2004
  process.exit(1);
1478
2005
  }
1479
2006
  const language = await promptInput(" Response language (ko/en/ja/zh, default: en): ");
1480
- const settingsDir = dirname2(userPath);
2007
+ const settingsDir = dirname3(userPath);
1481
2008
  mkdirSync2(settingsDir, { recursive: true });
1482
2009
  const settings = {
1483
2010
  provider: {