@kisev/agentomatic 12.0.1-dev.126.g020f34744898 → 12.0.1-dev.128.g273b5ff20349

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.
@@ -117,8 +117,9 @@ export declare function previewAgentProfileChange(request: AgentProfileRequest,
117
117
  export declare function applyAgentProfileChange(request: AgentProfileRequest, scope: Scope, cwd?: string, home?: string, options?: TransactionOptions & {
118
118
  expectedDigest?: string;
119
119
  }): Promise<AgentProfileResult>;
120
+ export declare function parseModelsOutput(stdout: string): string[];
120
121
  export declare function availableModels(): Promise<string[]>;
121
- export declare function availableModelVariants(model: string): Promise<string[]>;
122
+ export declare function availableModelVariants(model: string): Promise<string[] | null>;
122
123
  export declare function inspectFileMode(path: string): Promise<number | undefined>;
123
124
  export declare function readLegacyManifest(root: string): Promise<LegacyAgentOwnership | undefined>;
124
125
  export declare function applyBuiltAgentPlan(built: BuiltPlan, stateRoot: string, options?: TransactionOptions): Promise<void>;
@@ -752,8 +752,44 @@ export async function applyAgentProfileChange(request, scope, cwd = process.cwd(
752
752
  });
753
753
  }
754
754
  async function modelCatalog() {
755
- const query = new URLSearchParams({ "location[directory]": process.cwd() });
756
- const { stdout } = await execFileAsync("opencode", ["api", "get", `/api/model?${query}`], {
755
+ // The CLI catalog is the primary source: it works in a clean terminal, needs
756
+ // no background service, and never depends on the per-directory location
757
+ // filter that returns an empty list for some checkouts. If the background
758
+ // service is down, retry standalone before giving up with a remedy.
759
+ const models = await cliModels().catch((error) => cliModels(["--standalone"]).catch(() => {
760
+ throw new AgentProfileError("catalog_unavailable", `The opencode models CLI is unavailable (${error instanceof Error ? error.message : String(error)}); start opencode in a clean terminal or run 'opencode models' yourself to verify, then retry - or pass an explicit provider/model`);
761
+ }));
762
+ return models.map((model) => {
763
+ const [providerID, ...rest] = model.split("/");
764
+ return { providerID, id: rest.join("/") };
765
+ });
766
+ }
767
+ // Line-by-line `provider/model` output of `opencode models`; anything else in
768
+ // the stream is ignored. Exported for the parser test.
769
+ export function parseModelsOutput(stdout) {
770
+ return [
771
+ ...new Set(stdout
772
+ .split("\n")
773
+ .map((line) => line.trim())
774
+ .filter((line) => MODEL_PATTERN.test(line))),
775
+ ].sort();
776
+ }
777
+ async function cliModels(extra = []) {
778
+ const { stdout } = await execFileAsync("opencode", ["models", ...extra], {
779
+ timeout: 10_000,
780
+ encoding: "utf8",
781
+ maxBuffer: 10 * 1024 * 1024,
782
+ });
783
+ const models = parseModelsOutput(stdout);
784
+ if (!models.length)
785
+ throw new Error("opencode models printed no provider/model lines");
786
+ return models;
787
+ }
788
+ // The variants (effort) metadata still comes from the API - without the
789
+ // location filter: some directories answer an empty list to a located query,
790
+ // and that must never zero the cascade.
791
+ async function modelApiEntries() {
792
+ const { stdout } = await execFileAsync("opencode", ["api", "get", "/api/model"], {
757
793
  timeout: 10_000,
758
794
  encoding: "utf8",
759
795
  maxBuffer: 10 * 1024 * 1024,
@@ -783,7 +819,7 @@ export async function availableModels() {
783
819
  export async function availableModelVariants(model) {
784
820
  const selected = validateModel(model);
785
821
  try {
786
- const metadata = (await modelCatalog()).find((entry) => `${entry.providerID}/${entry.id}` === selected);
822
+ const metadata = (await modelApiEntries()).find((entry) => `${entry.providerID}/${entry.id}` === selected);
787
823
  if (!metadata)
788
824
  throw new Error("selected model is absent");
789
825
  if (metadata.variants === undefined)
@@ -793,8 +829,9 @@ export async function availableModelVariants(model) {
793
829
  throw new Error("invalid variants metadata");
794
830
  return metadata.variants.map((variant) => variant.id);
795
831
  }
796
- catch (error) {
797
- throw new AgentProfileError("catalog_unavailable", `OpenCode V2 model variants are unavailable: ${error instanceof Error ? error.message : String(error)}`);
832
+ catch {
833
+ // Unknown is honest: the cascade says so and allows a manual variant.
834
+ return null;
798
835
  }
799
836
  }
800
837
  export async function inspectFileMode(path) {
@@ -51,20 +51,13 @@ const commands = {
51
51
  description: "Remove owned components; disconnect the plugin by default; retain models and npm dependency unless selected.",
52
52
  options: ["disconnect", "removeDependency"],
53
53
  },
54
- agent: { description: "List agents or manage additional critics.", options: [] },
55
- "agent list": {
56
- description: "Show installed and saved profiles, models, ownership, collisions, and drift.",
54
+ agent: {
55
+ description: "Read-only agent overview: profiles, models, ownership, collisions, and drift (mutations: configure agent).",
57
56
  options: [],
58
57
  },
59
- "agent add-critic": {
60
- description: "Add critic-<name> interactively or with an explicit model.",
61
- options: ["provider", "model", "variant"],
62
- name: true,
63
- },
64
- "agent remove": {
65
- description: "Remove an additional critic; fixed roles cannot be removed here.",
58
+ "agent list": {
59
+ description: "Show installed and saved profiles, models, ownership, collisions, and drift.",
66
60
  options: [],
67
- name: true,
68
61
  },
69
62
  maintenance: {
70
63
  description: "Clean up retired files, repair selected components, or recover an interrupted transaction.",
@@ -89,6 +82,15 @@ const commands = {
89
82
  };
90
83
  const mutationOptions = new Set(["dryRun", "yes"]);
91
84
  const observations = new Set(["status", "doctor", "agent list", "catalog"]);
85
+ // Mutations were unified into configure agent; removed commands answer with a
86
+ // one-line pointer to it.
87
+ const removedCommands = {
88
+ "agent add-critic": "Removed command; agentomatic configure agent is the single mutation point for agent models and critics",
89
+ "agent remove": "Removed command; agentomatic configure agent is the single mutation point for agent models and critics",
90
+ };
91
+ function failRemoved(topic) {
92
+ return fail("invalid_input", removedCommands[topic]);
93
+ }
92
94
  function fail(code, message) {
93
95
  throw new InstallerError(code, message);
94
96
  }
@@ -125,6 +127,8 @@ async function promptCriticName() {
125
127
  }
126
128
  }
127
129
  function help(topic = "") {
130
+ if (topic && removedCommands[topic])
131
+ failRemoved(topic);
128
132
  if (topic && !commands[topic])
129
133
  fail("invalid_input", `Unknown command: ${topic}`);
130
134
  const visible = topic
@@ -177,7 +181,7 @@ function help(topic = "") {
177
181
  "Use status to inspect the installation, doctor to diagnose problems, and maintenance for explicit recovery.",
178
182
  "",
179
183
  "Examples:",
180
- ` ${shellCommand([...(topic ? topic.split(" ") : ["install"]), ...(commands[topic]?.name ? [topic === "agent remove" ? "critic-security" : "critic"] : []), ...(observations.has(topic) || ["agent", "maintenance"].includes(topic) ? [] : ["--dry-run"])])}`,
184
+ ` ${shellCommand([...(topic ? topic.split(" ") : ["install"]), ...(commands[topic]?.name ? ["critic"] : []), ...(observations.has(topic) || ["agent", "maintenance"].includes(topic) ? [] : ["--dry-run"])])}`,
181
185
  "Documentation: https://github.com/kisev/skills/blob/main/docs/how-to/opencode-integration.md",
182
186
  "",
183
187
  ].join("\n");
@@ -369,7 +373,14 @@ async function modelSelection(options, name, current) {
369
373
  const model = candidates[value(await selectOption("Model", candidates))];
370
374
  const variants = await availableModelVariants(model);
371
375
  let variant = null;
372
- if (variants.length) {
376
+ if (variants === null) {
377
+ // The variants metadata is unavailable: say so honestly and accept a
378
+ // manual variant instead of degrading the whole model selection.
379
+ process.stderr.write("Model variants are unknown (variants metadata unavailable); enter one manually or leave it empty.\n");
380
+ const answer = await promptText(current?.variant ? "Variant (empty keeps current)" : "Variant (optional)");
381
+ variant = (answer ? validateVariant(answer) : current?.variant) ?? null;
382
+ }
383
+ else if (variants.length) {
373
384
  const labels = variants.map((item) => item === current?.variant ? `${item} (default)` : item);
374
385
  const choice = value(await selectOption("Variant", ["(none)", ...labels], undefined, undefined, current?.variant ? variants.indexOf(current.variant) + 1 : 0));
375
386
  variant = choice === 0 ? null : (variants[choice - 1] ?? null);
@@ -506,18 +517,9 @@ async function deploy(options, repair = false) {
506
517
  ],
507
518
  };
508
519
  }
509
- let changes = [];
510
- if (!repair &&
511
- tty() &&
512
- value(await confirmQuestion("Configure agent models or additional critics now?"))) {
513
- const inventory = await listAgentProfiles(options.scope);
514
- changes = await profileDraft(options, [
515
- ...selection.agents,
516
- ...inventory.profiles
517
- .filter((profile) => profile.name.startsWith("critic-") && profile.ownership !== "user-owned")
518
- .map((profile) => profile.name),
519
- ]);
520
- }
520
+ // Install never mutates the critic panel: the summary ends with the panel
521
+ // table and the pointer to configure agent, the single mutation point.
522
+ const changes = [];
521
523
  const plan = await preview("install", options.scope, undefined, undefined, selection, !options.noDependency, changes);
522
524
  const config = await previewConfigSetup(integration, options.scope, undefined, undefined, false, false);
523
525
  const report = {
@@ -567,6 +569,7 @@ async function deploy(options, repair = false) {
567
569
  if (changes.length)
568
570
  process.stdout.write(`Staged profiles: ${terminalSafe(JSON.stringify(changes))}\n`);
569
571
  process.stdout.write("Components, npm, and application configuration are separate stages; a later failure does not roll back completed stages.\n");
572
+ await criticsEpilogue(options);
570
573
  }
571
574
  if (options.dryRun) {
572
575
  if (options.json)
@@ -605,8 +608,10 @@ async function deploy(options, repair = false) {
605
608
  };
606
609
  if (options.json)
607
610
  json(result);
608
- else
611
+ else {
609
612
  process.stdout.write(renderPlan(applied, { applied: true }) + renderConfigSetup(connected, { applied: true }));
613
+ await criticsEpilogue(options);
614
+ }
610
615
  }
611
616
  catch (error) {
612
617
  if (error instanceof LifecycleError &&
@@ -616,6 +621,16 @@ async function deploy(options, repair = false) {
616
621
  partial(options, error, completed, `Run status, then ${repair ? "maintenance repair" : "install"} with the same selected components; use configure integration for configuration-only continuation.`, completed.includes("owned-components-and-profiles") && plan.requires_restart);
617
622
  }
618
623
  }
624
+ // The critic panel epilogue: the table plus the canonical view and mutation
625
+ // pointers, printed wherever the panel matters (install, staged applies,
626
+ // configure integration).
627
+ async function criticsEpilogue(options) {
628
+ if (options.json)
629
+ return;
630
+ const inventory = await listAgentProfiles(options.scope);
631
+ process.stdout.write(renderCriticsTable(inventory));
632
+ process.stdout.write("Inspect any time: agentomatic agent list | Manage critics: agentomatic configure agent\n");
633
+ }
619
634
  function partial(options, error, completed, next, requiresRestart = false) {
620
635
  const code = error instanceof LifecycleError ? error.code : "internal_error";
621
636
  const message = error instanceof Error ? error.message : String(error);
@@ -672,8 +687,10 @@ async function configureIntegration(options) {
672
687
  plan: result,
673
688
  requires_restart: result.requires_restart,
674
689
  });
675
- else
690
+ else {
676
691
  process.stdout.write(renderConfigSetup(result, { applied: true }));
692
+ await criticsEpilogue(options);
693
+ }
677
694
  }
678
695
  catch (error) {
679
696
  partial(options, error, error instanceof LifecycleError && error.code === "npm_dependency_failed"
@@ -689,20 +706,18 @@ async function profileChange(options, request) {
689
706
  if (!options.json) {
690
707
  const args = request.changes
691
708
  ? ["configure", "critics"]
692
- : request.action === "critic-remove"
693
- ? ["agent", "remove", request.name]
694
- : [
695
- request.action === "critic-add" ? "agent" : "configure",
696
- request.action === "critic-add" ? "add-critic" : "agent",
697
- request.name,
698
- "--model",
699
- request.model,
700
- ...(request.variant
701
- ? ["--variant", request.variant]
702
- : request.action === "model-set"
703
- ? ["--clear-variant"]
704
- : []),
705
- ];
709
+ : [
710
+ "configure",
711
+ "agent",
712
+ request.name,
713
+ "--model",
714
+ request.model,
715
+ ...(request.variant
716
+ ? ["--variant", request.variant]
717
+ : request.action === "model-set"
718
+ ? ["--clear-variant"]
719
+ : []),
720
+ ];
706
721
  process.stdout.write(renderPlan(plan, {
707
722
  applied: false,
708
723
  applyHint: applyHint([...args, ...scopeArguments(options.scope)]),
@@ -720,8 +735,11 @@ async function profileChange(options, request) {
720
735
  });
721
736
  if (options.json)
722
737
  json(result);
723
- else
738
+ else {
724
739
  process.stdout.write(renderPlan(result.plan, { applied: true }));
740
+ // After a panel mutation the fresh table closes the loop.
741
+ await criticsEpilogue(options);
742
+ }
725
743
  }
726
744
  async function uninstall(options) {
727
745
  requireApply(options);
@@ -830,12 +848,16 @@ async function run(args) {
830
848
  return;
831
849
  }
832
850
  if (args.includes("--help") || args.includes("-h")) {
851
+ if (removedCommands[topic])
852
+ failRemoved(topic);
833
853
  if (!commands[topic])
834
854
  fail("invalid_input", `Unknown command: ${topic}`);
835
855
  parse(topic, args.filter((arg) => !["--help", "-h"].includes(arg)));
836
856
  process.stdout.write(help(topic));
837
857
  return;
838
858
  }
859
+ if (removedCommands[topic])
860
+ failRemoved(topic);
839
861
  if (!commands[topic]) {
840
862
  const match = [...new Set(Object.keys(commands).map((key) => key.split(" ", 1)[0]))].find((root) => root.startsWith(topic) || topic.startsWith(root));
841
863
  fail("invalid_input", `Unknown command: ${topic}.${match ? ` Did you mean '${match}'?` : ""} Use install, configure, status, doctor, uninstall, agent, maintenance, or catalog.`);
@@ -904,12 +926,11 @@ async function run(args) {
904
926
  process.exitCode = doctorExitCode(report);
905
927
  return;
906
928
  }
907
- if (topic === "configure critics" ||
908
- topic === "configure agent" ||
909
- topic === "agent add-critic") {
929
+ if (topic === "configure critics" || topic === "configure agent") {
910
930
  const inventory = await listAgentProfiles(options.scope);
911
- // configure critics is an alias; a nameless configure agent opens the same staged editor.
912
- if (topic === "configure critics" || (topic === "configure agent" && !options.name)) {
931
+ // configure critics is an alias; a nameless configure agent opens the staged
932
+ // editor - the single mutation point for models, additions, and removals.
933
+ if (!options.name) {
913
934
  requireApply(options);
914
935
  if (!options.json)
915
936
  process.stdout.write(renderCriticsTable(inventory));
@@ -926,30 +947,20 @@ async function run(args) {
926
947
  await profileChange(options, { action: "model-set", changes });
927
948
  return;
928
949
  }
929
- let name = options.name;
930
- if (!name) {
931
- if (topic === "agent add-critic")
932
- name = await promptCriticName();
933
- else {
934
- const names = inventory.profiles
935
- .filter((profile) => profile.ownership !== "user-owned")
936
- .map((profile) => profile.name);
937
- name = names[value(await selectOption("Agent", names))];
938
- }
950
+ const name = options.name;
951
+ const existing = inventory.profiles.find((profile) => profile.name === name);
952
+ if (!existing && !name.startsWith("critic-")) {
953
+ fail("invalid_input", `Unknown agent profile: ${name}`);
939
954
  }
940
- if (topic === "agent add-critic" && !name.startsWith("critic-"))
941
- name = `critic-${name}`;
942
- const request = await modelSelection(options, name, inventory.profiles.find((profile) => profile.name === name));
943
- await profileChange(options, {
944
- ...request,
945
- action: topic === "agent add-critic" ? "critic-add" : "model-set",
946
- });
947
- return;
948
- }
949
- if (topic === "agent remove") {
950
- if (!options.name)
951
- fail("invalid_input", "agent remove requires an additional critic name");
952
- await profileChange(options, { action: "critic-remove", name: options.name });
955
+ if (!existing) {
956
+ // configure agent on a fresh critic-<name> adds the critic: one command
957
+ // covers model-set for existing profiles and critic-add for new critics.
958
+ const request = await modelSelection(options, name, undefined);
959
+ await profileChange(options, { ...request, action: "critic-add" });
960
+ return;
961
+ }
962
+ const request = await modelSelection(options, name, existing);
963
+ await profileChange(options, { ...request, action: "model-set" });
953
964
  return;
954
965
  }
955
966
  if (topic === "maintenance recover") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kisev/agentomatic",
3
- "version": "12.0.1-dev.126.g020f34744898",
3
+ "version": "12.0.1-dev.128.g273b5ff20349",
4
4
  "description": "OpenCode integration and opt-in installer for portable Agent Skills.",
5
5
  "homepage": "https://github.com/kisev/skills#readme",
6
6
  "bugs": {
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@clack/prompts": "^1.8.1",
53
- "@kisev/safe-fs": "12.0.1-dev.126.g020f34744898",
53
+ "@kisev/safe-fs": "12.0.1-dev.128.g273b5ff20349",
54
54
  "commander": "15.0.0",
55
55
  "zod": "4.1.8"
56
56
  },