@dylanrussell/agent-router 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { Context } from '@opencode/plugin/tui/context';
2
+
1
3
  /**
2
4
  * Structural slice of opencode's TuiPluginApi.
3
5
  *
@@ -93,7 +95,7 @@ interface RouterTuiApi {
93
95
  declare const tui: (api: RouterTuiApi) => Promise<void>;
94
96
  declare const agentRouterTui: {
95
97
  id: string;
96
- tui: (api: RouterTuiApi) => Promise<void>;
98
+ setup(ctx: Context): Promise<(() => Promise<void>) | undefined>;
97
99
  };
98
100
 
99
101
  export { agentRouterTui as default, tui };
package/dist/tui.js CHANGED
@@ -90,7 +90,7 @@ function resolvePaths(options = {}) {
90
90
  return {
91
91
  opencodeConfigDir,
92
92
  opencodeJsonPath: path.join(opencodeConfigDir, "opencode.json"),
93
- tuiJsonPath: path.join(opencodeConfigDir, "tui.json"),
93
+ tuiJsonPath: path.join(opencodeConfigDir, "cli.json"),
94
94
  agentsDir,
95
95
  opencodeBackupsDir: path.join(opencodeConfigDir, ".backups"),
96
96
  routerHome,
@@ -14648,7 +14648,13 @@ var ConfigFileSchema = external_exports.object({
14648
14648
  stacksDir: external_exports.string().min(1).optional()
14649
14649
  }).strict();
14650
14650
  var OpencodeJsonSchema = external_exports.object({
14651
- plugin: external_exports.array(external_exports.string()).optional()
14651
+ plugin: external_exports.array(external_exports.string()).optional(),
14652
+ plugins: external_exports.array(
14653
+ external_exports.union([
14654
+ external_exports.string(),
14655
+ external_exports.object({ package: external_exports.string(), options: external_exports.record(external_exports.string(), external_exports.unknown()).optional() }).passthrough()
14656
+ ])
14657
+ ).optional()
14652
14658
  }).passthrough();
14653
14659
 
14654
14660
  // src/core/config.ts
@@ -15046,6 +15052,11 @@ var RESERVED_AGENT_KEYS = /* @__PURE__ */ new Set([
15046
15052
  "mode",
15047
15053
  "description",
15048
15054
  "permission",
15055
+ "permissions",
15056
+ "disabled",
15057
+ "disable",
15058
+ "system",
15059
+ "hidden",
15049
15060
  "color",
15050
15061
  "tools",
15051
15062
  "prompt",
@@ -15294,7 +15305,7 @@ import { execFile } from "child_process";
15294
15305
  import { promisify } from "util";
15295
15306
  var execFileAsync = promisify(execFile);
15296
15307
  var DEFAULT_RUNNER = async () => {
15297
- const { stdout } = await execFileAsync("opencode", ["models"], {
15308
+ const { stdout } = await execFileAsync("opencode", ["api", "model.list"], {
15298
15309
  encoding: "utf8",
15299
15310
  timeout: 3e4,
15300
15311
  maxBuffer: 4 * 1024 * 1024
@@ -15303,6 +15314,17 @@ var DEFAULT_RUNNER = async () => {
15303
15314
  };
15304
15315
  function parseModelList(stdout) {
15305
15316
  const out = /* @__PURE__ */ new Set();
15317
+ if (/^[\[{]/.test(stdout.trimStart())) {
15318
+ const parsed = JSON.parse(stdout);
15319
+ const models = Array.isArray(parsed) ? parsed : parsed.data;
15320
+ for (const model of models) {
15321
+ if (model.enabled === false) continue;
15322
+ const id = `${model.providerID}/${model.id}`;
15323
+ out.add(id);
15324
+ for (const variant of model.variants ?? []) out.add(`${id}#${variant.id}`);
15325
+ }
15326
+ return out;
15327
+ }
15306
15328
  for (const raw of stdout.split(/\r?\n/)) {
15307
15329
  const line = raw.trim();
15308
15330
  if (!line) continue;
@@ -15327,6 +15349,19 @@ async function validateStack(stack, options = {}) {
15327
15349
  const available = parseModelList(stdout);
15328
15350
  const refs = collectModelRefs(stack);
15329
15351
  const missing = refs.filter((r) => !available.has(r.modelId));
15352
+ if (/^[\[{]/.test(stdout.trimStart())) {
15353
+ for (const [agent, entry] of Object.entries(stack.agents)) {
15354
+ for (const [index, candidate] of [entry, ...entry.fallbacks ?? []].entries()) {
15355
+ if (!candidate.variant) continue;
15356
+ const modelId = `${candidate.model}#${candidate.variant}`;
15357
+ if (!available.has(modelId))
15358
+ missing.push({
15359
+ path: index === 0 ? `agents.${agent}.variant` : `agents.${agent}.fallbacks.${index - 1}.variant`,
15360
+ modelId
15361
+ });
15362
+ }
15363
+ }
15364
+ }
15330
15365
  return { ok: missing.length === 0, checked: refs.length, missing, available };
15331
15366
  }
15332
15367
  async function validateStackOrThrow(stackName, stack, options = {}) {
@@ -15391,8 +15426,15 @@ async function applyStack(paths, name, options = {}) {
15391
15426
  for (const [agent, entry] of Object.entries(target.agents)) {
15392
15427
  const { filePath, content, model } = await readAgentFileStrict(paths.agentsDir, agent);
15393
15428
  const wantOptions = entryOptions(entry);
15429
+ let wantModel = entry.model;
15430
+ if (model.includes("#") || wantModel.includes("#")) {
15431
+ const [base, inline] = wantModel.split("#");
15432
+ const variant = "variant" in entry ? entry.variant : inline ?? model.split("#")[1];
15433
+ wantModel = `${base}${variant ? `#${variant}` : ""}`;
15434
+ wantOptions.variant = null;
15435
+ }
15394
15436
  const afterOptions = setFrontmatterOptions(content, wantOptions);
15395
- const nextContent = entry.model === model ? afterOptions : setFrontmatterModel(afterOptions, entry.model);
15437
+ const nextContent = wantModel === model ? afterOptions : setFrontmatterModel(afterOptions, wantModel);
15396
15438
  pending.push({ agent, filePath, next: nextContent === content ? null : nextContent });
15397
15439
  }
15398
15440
  const prevState = await readState(paths.statePath);
@@ -15459,6 +15501,10 @@ function entriesToStackAgents(entries) {
15459
15501
  for (const [k, v] of Object.entries(options)) {
15460
15502
  if (k !== "fallbacks") stackEntry[k] = v;
15461
15503
  }
15504
+ if (model.includes("#")) {
15505
+ stackEntry.model = model.slice(0, model.indexOf("#"));
15506
+ stackEntry.variant = model.slice(model.indexOf("#") + 1);
15507
+ }
15462
15508
  out[name] = stackEntry;
15463
15509
  }
15464
15510
  return out;
@@ -16014,7 +16060,122 @@ var tui = async (api) => {
16014
16060
  };
16015
16061
  var agentRouterTui = {
16016
16062
  id: "agent-router:tui",
16017
- tui
16063
+ async setup(ctx) {
16064
+ const solid = await importHostSolid();
16065
+ if (!solid) return;
16066
+ const cleanups = [];
16067
+ const [revision, updateRevision] = ctx.storage.memory("sidebar-revision", {
16068
+ initial: { value: 0 }
16069
+ });
16070
+ const api = {
16071
+ slots: {
16072
+ register: ({ slots }) => {
16073
+ for (const render of Object.values(slots)) {
16074
+ cleanups.push(
16075
+ ctx.ui.slot({
16076
+ append: "sidebar.content",
16077
+ render: () => {
16078
+ const node = solid.createElement("box");
16079
+ solid.setProp(node, "flexDirection", "column");
16080
+ solid.insert(node, () => {
16081
+ void revision.value;
16082
+ return render();
16083
+ });
16084
+ return node;
16085
+ }
16086
+ })
16087
+ );
16088
+ }
16089
+ }
16090
+ },
16091
+ renderer: {
16092
+ requestRender: () => {
16093
+ updateRevision((draft) => {
16094
+ draft.value++;
16095
+ });
16096
+ ctx.renderer.requestRender();
16097
+ }
16098
+ },
16099
+ ui: {
16100
+ toast: (input) => ctx.ui.toast.show(input),
16101
+ dialog: {
16102
+ replace: (render) => {
16103
+ render();
16104
+ },
16105
+ clear: () => ctx.ui.dialog.clear()
16106
+ },
16107
+ DialogSelect: (props) => {
16108
+ void ctx.ui.dialog.select({
16109
+ title: props.title,
16110
+ options: props.options.map((option) => ({
16111
+ title: option.title,
16112
+ value: option.value,
16113
+ ...option.description ? { description: option.description } : {},
16114
+ ...option.category ? { category: option.category } : {},
16115
+ ...option.disabled !== void 0 ? { disabled: option.disabled } : {}
16116
+ })),
16117
+ current: props.current
16118
+ }).then((value) => {
16119
+ const option = props.options.find((option2) => option2.value === value);
16120
+ if (option) {
16121
+ option.onSelect?.();
16122
+ props.onSelect?.(option);
16123
+ }
16124
+ });
16125
+ return null;
16126
+ },
16127
+ DialogConfirm: (props) => {
16128
+ void ctx.ui.dialog.confirm({ title: props.title, message: props.message }).then((yes) => {
16129
+ if (yes) props.onConfirm?.();
16130
+ else props.onCancel?.();
16131
+ });
16132
+ return null;
16133
+ }
16134
+ },
16135
+ command: {
16136
+ register: (build) => cleanups.push(
16137
+ ctx.ui.slot({
16138
+ append: "app",
16139
+ render: () => {
16140
+ ctx.keymap.layer(() => ({
16141
+ mode: "global",
16142
+ commands: build().map((command) => ({
16143
+ id: command.value,
16144
+ title: command.title,
16145
+ group: command.category ?? "agent-router",
16146
+ palette: true,
16147
+ ...command.slash ? {
16148
+ slash: {
16149
+ name: command.slash.name,
16150
+ aliases: [...command.slash.aliases ?? []]
16151
+ }
16152
+ } : {},
16153
+ run: () => command.onSelect?.()
16154
+ }))
16155
+ }));
16156
+ return null;
16157
+ }
16158
+ })
16159
+ )
16160
+ },
16161
+ lifecycle: { onDispose: (cleanup) => cleanups.push(cleanup) },
16162
+ state: {
16163
+ get provider() {
16164
+ const models = ctx.data.location.model.list(ctx.location) ?? [];
16165
+ return [...new Set(models.map((model) => model.providerID))].map((id) => ({
16166
+ id,
16167
+ models: Object.fromEntries(
16168
+ models.filter((model) => model.providerID === id).map((model) => [model.id, model])
16169
+ )
16170
+ }));
16171
+ }
16172
+ }
16173
+ };
16174
+ await tui(api);
16175
+ return async () => {
16176
+ for (const cleanup of cleanups.reverse()) await cleanup();
16177
+ };
16178
+ }
16018
16179
  };
16019
16180
  var tui_default = agentRouterTui;
16020
16181
  export {