@link-assistant/hive-mind 2.18.0 → 2.19.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ac07514: Stop the model list from ageing with the release: add the models that shipped since 2.17.0, hot-load the live catalogue from every source that costs nothing to read, expose it as `/models` and `hive-models`, teach `--use-router` both of the router's route dialects, and check the agentic CLI for a newer version before a task starts (issue #2202).
8
+
9
+ A provider can ship a model on a Tuesday, and until now Hive Mind could not name it until the next release: `src/models/index.mjs` was a static table and `--model` validated against nothing else. Six changes close that gap, none of which can spend a token or fail a run.
10
+
11
+ - **The new models are in the bundled table.** Fable 5.1, Mythos 5.1, GPT-6 Astra and GPT-5.6 Cyber, with their aliases, context windows and fallback chains. The additions pushed `src/models/index.mjs` past the 1350-line early warning of `scripts/check-file-line-limits.sh`, so the catalogue itself moved to `src/models/catalog.mjs` and is re-exported — the public surface is unchanged.
12
+ - **The catalogue hot-loads.** `src/model-catalogue.lib.mjs` merges six sources in a fixed precedence: the Link.Assistant Router's live catalogue, `codex debug models`, Anthropic's and OpenAI's `GET /v1/models`, models.dev for specifications, and the bundled table as the offline floor. models.dev only annotates; it never adds a model to the available list. Answers are cached per source and per tool for at least an hour, and `HIVE_MIND_MODEL_CATALOGUE_TTL_MINUTES` can only raise that floor.
13
+ - **Listing models cannot cost a token, structurally.** `assertTokenFreeSource` rejects any source that has not explicitly declared `billable: false`, so a source cannot become billable by omission, and `assertTokenFreeUrl` rejects any URL whose path is a completion endpoint no matter which descriptor carried it — a typo turning `/v1/models` into `/v1/messages` throws instead of spending. The four extraction methods that were considered and rejected, including driving either CLI's `/model` picker through its TUI, are recorded with their reasons so a future contributor does not re-derive them.
14
+ - **`/models` and `hive-models` show the whole spectrum.** Models are grouped as bundled-and-live, hot-loaded (a live source has it, this installation does not ship it), and bundled-only (shipped, unconfirmed), with `--tool`, `--details`, `--refresh`, `--json`, and `--all`. A footer names every source actually consulted, its outcome, and the age of the cache, so "why don't I see X" is answerable from the output — and a source that failed is described from its stderr, never by quoting the command line, so the router's leased token cannot ride a failure message into the footer. In Telegram the same flags are accepted the way a chat writes them: `/models codex`, `/models --tool codex` and `/models --tool=codex` are one command.
15
+ - **`--use-router` speaks both route dialects.** Router 1.0 removed every root, `/v1/*` and overlapping `/api/*` alias and moved each service under `/api/services/<service>/…` — measured, not assumed: on `1.2.0` every path that answers on `0.119.0` is a 404 and vice versa. `src/router-routes.lib.mjs` derives every base URL, the health probe and the catalogue path from the pinned image, so the pin can move in one line. It stays on `0.x` today, at `0.125.4`, because router `1.x` has no `gh`-reachable REST base and `gh` exposes no path-prefix option; the trade is reported by `describeRouterCoverageGaps` rather than discovered at runtime, and the upstream issues are filed.
16
+ - **A stale CLI is refreshed before the run needs it.** `/solve`, `/hive`, `/task`, `/fix`, `/models` and `hive-models` check the agentic CLI they are about to drive, because an outdated binary is the usual reason a brand-new model name is rejected. The check is throttled to once every six hours, narrowed to the one CLI in play, deferred entirely while other tasks are running, skipped for `--dry-run` and `--only-prepare-command`, and never fatal. Each run excludes its own task from the idle gate — otherwise a `/solve` asking for an update would find itself in the process table and defer forever. Opt out with `--no-tool-update` or `HIVE_MIND_AGENTIC_CLI_AUTO_UPDATE=0`.
17
+
18
+ Documentation is in `docs/MODELS.md` and its `zh`/`hi`/`ru` siblings; the full analysis — the requirement-by-requirement plan, the router route measurement, and what shipped differently from the plan — is in `docs/case-studies/issue-2202/`.
19
+
3
20
  ## 2.18.0
4
21
 
5
22
  ### Minor Changes
package/README.hi.md CHANGED
@@ -515,6 +515,8 @@ Free Models via Kilo Gateway (with --tool agent):
515
515
 
516
516
  > **📖 मुफ्त मॉडल गाइड**: OpenCode Zen और Kilo Gateway प्रदाताओं सहित सभी मुफ्त मॉडलों के बारे में व्यापक जानकारी के लिए [docs/FREE_MODELS.hi.md](./docs/FREE_MODELS.hi.md) देखें।
517
517
 
518
+ > **📖 लाइव मॉडल सूची**: यह देखने के लिए कि इस समय कौन-से मॉडल उपलब्ध हैं — उनमें वे भी जो इस installation के प्रकाशित होने के बाद जारी हुए — `hive-models` चलाएँ (या Telegram में `/models`)। देखें [docs/MODELS.hi.md](./docs/MODELS.hi.md)।
519
+
518
520
  #### `/hive` - Hive ऑर्केस्ट्रेशन चलाएँ
519
521
 
520
522
  ```
package/README.md CHANGED
@@ -533,6 +533,8 @@ See [docs/CONFIGURATION.md](./docs/CONFIGURATION.md) for the full per-tool defau
533
533
 
534
534
  > **📖 Free Models Guide**: See [docs/FREE_MODELS.md](./docs/FREE_MODELS.md) for comprehensive information about all free models including OpenCode Zen and Kilo Gateway providers.
535
535
 
536
+ > **📖 Live model list**: run `hive-models` (or `/models` in Telegram) to see which models are reachable right now, including ones released after this installation was published. See [docs/MODELS.md](./docs/MODELS.md).
537
+
536
538
  #### `/hive` - Run Hive Orchestration
537
539
 
538
540
  ```
package/README.ru.md CHANGED
@@ -516,6 +516,8 @@ Free Models via Kilo Gateway (with --tool agent):
516
516
 
517
517
  > **📖 Руководство по бесплатным моделям**: см. [docs/FREE_MODELS.ru.md](./docs/FREE_MODELS.ru.md) для получения полной информации обо всех бесплатных моделях, включая провайдеры OpenCode Zen и Kilo Gateway.
518
518
 
519
+ > **📖 Живой список моделей**: запустите `hive-models` (или `/models` в Telegram), чтобы увидеть, какие модели доступны прямо сейчас, включая вышедшие уже после публикации этой установки. См. [docs/MODELS.ru.md](./docs/MODELS.ru.md).
520
+
519
521
  #### `/hive` — Запуск оркестрации Hive
520
522
 
521
523
  ```
package/README.zh.md CHANGED
@@ -512,6 +512,8 @@ Free Models via Kilo Gateway (with --tool agent):
512
512
 
513
513
  > **📖 免费模型指南**:有关所有免费模型(包括 OpenCode Zen 和 Kilo Gateway 提供商)的全面信息,请参见 [docs/FREE_MODELS.zh.md](./docs/FREE_MODELS.zh.md)。
514
514
 
515
+ > **📖 实时模型列表**:运行 `hive-models`(或在 Telegram 中使用 `/models`)即可查看当前可用的模型,包括本次安装发布之后才推出的模型。参见 [docs/MODELS.zh.md](./docs/MODELS.zh.md)。
516
+
515
517
  #### `/hive` - 运行蜂群编排
516
518
 
517
519
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.18.0",
3
+ "version": "2.19.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -14,6 +14,7 @@
14
14
  "configure-claude": "./src/configure-claude.mjs",
15
15
  "start-screen": "./src/start-screen.mjs",
16
16
  "hive-screens": "./src/hive-screens.mjs",
17
+ "hive-models": "./src/hive-models.mjs",
17
18
  "hive-telegram-bot": "./src/telegram-bot.mjs"
18
19
  },
19
20
  "scripts": {
@@ -30,7 +31,7 @@
30
31
  "changeset": "changeset",
31
32
  "changeset:version": "changeset version",
32
33
  "changeset:publish": "npm run build:pre && changeset publish",
33
- "build:pre": "chmod +x src/hive.mjs && chmod +x src/solve.mjs && chmod +x src/task.mjs && chmod +x src/fix.mjs && chmod +x src/cleanup.mjs && chmod +x src/review.mjs && chmod +x src/configure-claude.mjs && chmod +x src/start-screen.mjs && chmod +x src/hive-screens.mjs && chmod +x src/telegram-bot.mjs",
34
+ "build:pre": "chmod +x src/hive.mjs && chmod +x src/solve.mjs && chmod +x src/task.mjs && chmod +x src/fix.mjs && chmod +x src/cleanup.mjs && chmod +x src/review.mjs && chmod +x src/configure-claude.mjs && chmod +x src/start-screen.mjs && chmod +x src/hive-screens.mjs && chmod +x src/hive-models.mjs && chmod +x src/telegram-bot.mjs",
34
35
  "prepare": "husky"
35
36
  },
36
37
  "repository": {
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Pre-flight CLI freshness check (issue #2202, R6).
3
+ *
4
+ * R6: "/models and each /solve and other commands that relevant for
5
+ * claude/codex tools should check if new version available, and before
6
+ * starting task execution or before providing new models list - we should
7
+ * update them."
8
+ *
9
+ * `updateAgenticClisWhenIdle` already knows how to do the refresh safely — it
10
+ * throttles registry reads, takes a state lock, and refuses to swap a binary
11
+ * out from under a running task. What it lacked was a caller other than the
12
+ * Telegram maintenance tick, and two things a command entry point needs:
13
+ *
14
+ * 1. **Narrowing.** `hive-models --tool codex` should not reinstall Gemini.
15
+ * 2. **Not counting itself as busy.** The idle gate scans `/proc` for running
16
+ * solve/task processes by issue reference. A solve run that checks for
17
+ * updates after it has started would find *itself* and defer forever, so
18
+ * the caller passes its own task reference to be ignored.
19
+ *
20
+ * Everything here is best-effort: a refresh failure must never stop the command
21
+ * the operator actually asked for.
22
+ *
23
+ * @see https://github.com/link-assistant/hive-mind/issues/2202
24
+ */
25
+
26
+ import { AGENTIC_CLI_TARGETS, isAgenticCliAutoUpdateEnabled, updateAgenticClisWhenIdle } from './agentic-cli-updater.lib.mjs';
27
+
28
+ const KNOWN_TOOL_IDS = new Set(AGENTIC_CLI_TARGETS.map(target => target.id));
29
+
30
+ /** Tool aliases Hive Mind commands use that are not the updater's target id. */
31
+ export const FRESHNESS_TOOL_ALIASES = Object.freeze({
32
+ 'claude-code': 'claude',
33
+ 'gemini-cli': 'gemini',
34
+ 'qwen-code': 'qwen',
35
+ 'github-copilot': 'copilot',
36
+ 'opencode-ai': 'opencode',
37
+ });
38
+
39
+ /** Normalize whatever a command calls its tool into updater target ids. */
40
+ export const resolveFreshnessTools = tools => {
41
+ const requested = (Array.isArray(tools) ? tools : [tools])
42
+ .flatMap(entry =>
43
+ String(entry ?? '')
44
+ .split(',')
45
+ .map(part => part.trim().toLowerCase())
46
+ )
47
+ .filter(Boolean);
48
+ const resolved = [];
49
+ for (const entry of requested) {
50
+ const id = FRESHNESS_TOOL_ALIASES[entry] ?? entry;
51
+ if (KNOWN_TOOL_IDS.has(id) && !resolved.includes(id)) resolved.push(id);
52
+ }
53
+ return resolved;
54
+ };
55
+
56
+ /** `https://github.com/o/r/issues/7` → `{owner:'o', repo:'r', number:7}`. */
57
+ export const parseTaskRef = value => {
58
+ if (!value) return null;
59
+ if (typeof value === 'object') {
60
+ const number = Number(value.number);
61
+ if (!value.owner || !value.repo || !Number.isFinite(number)) return null;
62
+ return { owner: String(value.owner), repo: String(value.repo), number };
63
+ }
64
+ const match = String(value).match(/github\.com\/([^/\s]+)\/([^/\s]+)\/(?:issues|pull)\/(\d+)/i);
65
+ if (!match) return null;
66
+ return { owner: match[1], repo: match[2], number: Number(match[3]) };
67
+ };
68
+
69
+ const sameRef = (a, b) => a.owner.toLowerCase() === b.owner.toLowerCase() && a.repo.toLowerCase() === b.repo.toLowerCase() && a.number === b.number;
70
+
71
+ /**
72
+ * Refresh the agentic CLIs a command is about to drive.
73
+ *
74
+ * Never throws: every failure path returns a status the caller can log and
75
+ * ignore. Statuses come from `updateAgenticClisWhenIdle`
76
+ * (`checked`/`throttled`/`busy`/`disabled`) plus `skipped` for a caller opt-out
77
+ * and `error` when the refresh itself blew up.
78
+ *
79
+ * @param {object} options
80
+ * @param {string|string[]} options.tools tool ids/aliases the command needs
81
+ * @param {boolean} options.enabled false for `--no-update`
82
+ * @param {Array} options.ignoreTasks task refs or GitHub URLs that are *this* run
83
+ */
84
+ export const ensureAgenticCliFreshness = async ({ tools = [], env = process.env, log = null, verbose = false, force = false, enabled = true, ignoreTasks = [], getActiveTasksImpl = null, updateImpl = updateAgenticClisWhenIdle, minIntervalMs = undefined } = {}) => {
85
+ const only = resolveFreshnessTools(tools);
86
+ if (!enabled) return { status: 'skipped', reason: 'the caller disabled the update check (--no-update)', tools: only, updated: [], upToDate: [], failed: [] };
87
+ if (!isAgenticCliAutoUpdateEnabled(env)) return { status: 'disabled', reason: 'HIVE_MIND_AGENTIC_CLI_AUTO_UPDATE is off', tools: only, updated: [], upToDate: [], failed: [] };
88
+
89
+ const ignored = ignoreTasks.map(parseTaskRef).filter(Boolean);
90
+ let activeTasks = getActiveTasksImpl;
91
+ if (ignored.length > 0) {
92
+ const inner = getActiveTasksImpl ?? (await import('./cleanup.os.lib.mjs')).getActiveTasks;
93
+ activeTasks = async options => {
94
+ const tasks = await inner(options);
95
+ return tasks.filter(task => !ignored.some(ref => sameRef(ref, { owner: task.owner, repo: task.repo, number: Number(task.number) })));
96
+ };
97
+ }
98
+
99
+ try {
100
+ const result = await updateImpl({ env, log, verbose, force, only, ...(activeTasks ? { getActiveTasksImpl: activeTasks } : {}), ...(minIntervalMs === undefined ? {} : { minIntervalMs }) });
101
+ return { tools: only, updated: [], upToDate: [], failed: [], ...result };
102
+ } catch (error) {
103
+ const message = String(error?.message ?? error);
104
+ if (verbose && log) await log(`[VERBOSE] agentic-cli-freshness: refresh failed — ${message}`);
105
+ return { status: 'error', reason: message, tools: only, updated: [], upToDate: [], failed: [] };
106
+ }
107
+ };
108
+
109
+ /** One human-readable line summarising a freshness result, or null when there is nothing to say. */
110
+ export const describeFreshnessResult = result => {
111
+ if (!result) return null;
112
+ if (result.updated?.length > 0) return `⬆️ Updated ${result.updated.map(entry => `${entry.id} ${entry.from} → ${entry.to}`).join(', ')}`;
113
+ if (result.failed?.length > 0) return `⚠️ Could not update ${result.failed.map(entry => entry.id).join(', ')}`;
114
+ if (result.status === 'busy') return 'Skipped the CLI update check: other tasks are running.';
115
+ return null;
116
+ };
117
+
118
+ export default { FRESHNESS_TOOL_ALIASES, describeFreshnessResult, ensureAgenticCliFreshness, parseTaskRef, resolveFreshnessTools };
@@ -81,10 +81,14 @@ const parseIdList = value =>
81
81
  * `HIVE_MIND_AGENTIC_CLI_UPDATE_ONLY` is an allow-list and
82
82
  * `HIVE_MIND_AGENTIC_CLI_UPDATE_EXCLUDE` a deny-list, both by target id.
83
83
  */
84
- export const listAgenticCliUpdateTargets = (env = process.env) => {
84
+ export const listAgenticCliUpdateTargets = (env = process.env, { only: requested = [] } = {}) => {
85
85
  const only = parseIdList(env.HIVE_MIND_AGENTIC_CLI_UPDATE_ONLY);
86
86
  const excluded = new Set(parseIdList(env.HIVE_MIND_AGENTIC_CLI_UPDATE_EXCLUDE));
87
- return AGENTIC_CLI_TARGETS.filter(target => (only.length === 0 || only.includes(target.id)) && !excluded.has(target.id));
87
+ // A caller-supplied narrowing (issue #2202, R6: refresh the CLIs a command is
88
+ // about to drive) intersects with the operator's allow-list rather than
89
+ // overriding it — an operator who excluded a CLI still gets it excluded.
90
+ const caller = parseIdList(Array.isArray(requested) ? requested.join(',') : requested);
91
+ return AGENTIC_CLI_TARGETS.filter(target => (only.length === 0 || only.includes(target.id)) && (caller.length === 0 || caller.includes(target.id)) && !excluded.has(target.id));
88
92
  };
89
93
 
90
94
  /** First semantic version in a CLI's `--version` output, which is rarely bare. */
@@ -147,7 +151,7 @@ export const installAgenticCli = async (target, { run = execFileAsync, timeoutMs
147
151
  *
148
152
  * @returns {Promise<{status: 'disabled'|'busy'|'throttled'|'checked', updated: object[], upToDate: object[], failed: object[]}>}
149
153
  */
150
- export const updateAgenticClisWhenIdle = async ({ env = process.env, fsImpl = fs, run = execFileAsync, log = null, verbose = false, getActiveTasksImpl = null, now = () => new Date(), minIntervalMs = DEFAULT_CLI_UPDATE_INTERVAL_MS, force = false, lockOptions = {} } = {}) => {
154
+ export const updateAgenticClisWhenIdle = async ({ env = process.env, fsImpl = fs, run = execFileAsync, log = null, verbose = false, getActiveTasksImpl = null, now = () => new Date(), minIntervalMs = DEFAULT_CLI_UPDATE_INTERVAL_MS, force = false, only = [], lockOptions = {} } = {}) => {
151
155
  if (!isAgenticCliAutoUpdateEnabled(env)) {
152
156
  if (verbose && log) await log('[VERBOSE] agentic-cli-updater: disabled by HIVE_MIND_AGENTIC_CLI_AUTO_UPDATE');
153
157
  return { status: 'disabled', updated: [], upToDate: [], failed: [] };
@@ -176,7 +180,7 @@ export const updateAgenticClisWhenIdle = async ({ env = process.env, fsImpl = fs
176
180
  const failed = [];
177
181
  const tools = { ...state.tools };
178
182
 
179
- for (const target of listAgenticCliUpdateTargets(env)) {
183
+ for (const target of listAgenticCliUpdateTargets(env, { only })) {
180
184
  const installed = await readInstalledCliVersion(target, { run });
181
185
  if (!installed) {
182
186
  // Not installed on this host (image variants differ); nothing to refresh.
@@ -49,8 +49,24 @@ export const dockerOk = async (run, args, options) => {
49
49
  }
50
50
  };
51
51
 
52
+ /**
53
+ * Mask the secrets a `docker` argv carries.
54
+ *
55
+ * `execFile` builds a failed command's message as "Command failed: <argv…>",
56
+ * and the argvs assembled here carry real secrets: the router's
57
+ * `--env TOKEN_SECRET=…`, a leased `--env ROUTER_CATALOGUE_TOKEN=…`, a
58
+ * provider's `--api-key …`. stderr is preferred over that message precisely
59
+ * because it describes the failure without the command line, but stderr is
60
+ * empty when the process is killed on a timeout — so whatever survives is
61
+ * masked before anyone reads it.
62
+ */
63
+ const maskDockerArgvSecrets = text =>
64
+ String(text ?? '')
65
+ .replace(/(--env[= ])([A-Za-z_][A-Za-z0-9_]*)=(\S+)/g, '$1$2=***')
66
+ .replace(/(--(?:api-key|token|secret|password)[= ])(\S+)/g, '$1***');
67
+
52
68
  /** The message a failed `docker` invocation should be reported with. */
53
- export const dockerErrorMessage = error => error?.stderr?.toString?.().trim() || error?.message || String(error);
69
+ export const dockerErrorMessage = error => maskDockerArgvSecrets(error?.stderr?.toString?.().trim() || error?.message || String(error));
54
70
 
55
71
  /**
56
72
  * Inspect a container without treating "absent" as an error.
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Shared runner for the `hive-models` bin command (issue #2202, R5).
5
+ *
6
+ * R5 asks for a listing that merges what this installation ships with what the
7
+ * providers are serving right now, "from fully supported, to hot loaded", per
8
+ * tool. This module is the CLI half of that; `telegram-models-command.lib.mjs`
9
+ * is the `/models` half, and both render through
10
+ * `model-catalogue-render.lib.mjs` so the two can never disagree.
11
+ *
12
+ * R6 is honoured here too: before printing a catalogue the runner gives the
13
+ * agentic CLIs a chance to update, because a stale `codex` binary is exactly
14
+ * what makes a new model look unavailable.
15
+ *
16
+ * @see https://github.com/link-assistant/hive-mind/issues/2202
17
+ */
18
+
19
+ import { ensureAgenticCliFreshness, describeFreshnessResult } from './agentic-cli-freshness.lib.mjs';
20
+ import { parseCliArgumentsWithLino } from './cli-arguments.lib.mjs';
21
+ import { MODEL_CATALOGUE_TOOLS, getMergedModelCatalogue } from './model-catalogue.lib.mjs';
22
+ import { formatModelCatalogueText } from './model-catalogue-render.lib.mjs';
23
+
24
+ export const HIVE_MODELS_HELP = `Usage: hive-models [--tool <name>...] [--refresh] [--details] [--json] [--no-update] [--verbose]
25
+
26
+ List the models Hive Mind can drive, merged from every source that can be read
27
+ without spending a token — the router's live catalogue, the provider listing
28
+ endpoints, the codex CLI's own catalogue, models.dev metadata, and the models
29
+ bundled with this installation.
30
+
31
+ Models are grouped so it is obvious what each one is:
32
+ Bundled and live shipped here and confirmed reachable now
33
+ Hot loaded a live source has it, this installation does not ship it
34
+ Bundled only shipped here, no live source confirmed it
35
+
36
+ Options:
37
+ -t, --tool <name> Restrict to one tool (${MODEL_CATALOGUE_TOOLS.join(', ')}).
38
+ Repeatable; defaults to every tool.
39
+ --refresh Ignore the cached answer and re-read every live source
40
+ --details Show context window, pricing, and which source had it
41
+ --json Print machine-readable JSON instead of text
42
+ --no-update Do not check the agentic CLIs for a newer version first
43
+ (also spelled --no-tool-update, as in /solve and /task)
44
+ -v, --verbose Print diagnostics to stderr
45
+ -h, --help Show this help and exit
46
+
47
+ Environment:
48
+ HIVE_MIND_MODELS_HOT_LOAD=0 Only list the bundled catalogue
49
+ HIVE_MIND_MODELS_ROUTER=0 Skip the router source specifically
50
+ HIVE_MIND_MODEL_CATALOGUE_TTL_MINUTES Raise the 60 minute cache lifetime
51
+ HIVE_MIND_AGENTIC_CLI_AUTO_UPDATE=0 Never update the CLIs
52
+
53
+ Examples:
54
+ hive-models # every tool, cached answers
55
+ hive-models --tool codex # just codex
56
+ hive-models --tool claude --details --refresh
57
+ hive-models --json | jq '.tools.claude.liveOnly'
58
+
59
+ Reference:
60
+ https://github.com/link-assistant/hive-mind/issues/2202
61
+ `;
62
+
63
+ const VALUE_FLAGS = new Set(['--tool', '-t']);
64
+ const BOOLEAN_FLAGS = new Set(['--refresh', '--details', '--json', '--no-update', '--no-tool-update', '--verbose', '-v', '--help', '-h']);
65
+
66
+ // `/solve`, `/hive` and `/task` spell the opt-out `--no-tool-update` (it lives in
67
+ // their `tool-*` namespace). Accept that spelling here too, so the flag an
68
+ // operator already knows works everywhere it makes sense (issue #2202, R6).
69
+ const normaliseUpdateFlag = arg => (arg === '--no-tool-update' ? '--no-update' : arg);
70
+
71
+ const createHiveModelsYargsConfig = yargsInstance => yargsInstance.usage('Usage: hive-models [--tool <name>...] [--refresh] [--details] [--json] [--no-update] [--verbose]').option('tool', { type: 'array', alias: 't', default: [] }).option('refresh', { type: 'boolean', default: false }).option('details', { type: 'boolean', default: false }).option('json', { type: 'boolean', default: false }).option('update', { type: 'boolean', default: true }).option('verbose', { type: 'boolean', alias: 'v', default: false }).option('help', { type: 'boolean', alias: 'h', default: false }).help(false).version(false).strict(false);
72
+
73
+ /**
74
+ * Parse argv for `hive-models`. Returns `error` as a string rather than
75
+ * throwing, so the bin can print it and exit non-zero.
76
+ */
77
+ export const parseHiveModelsArgs = argv => {
78
+ const result = { tools: [], refresh: false, details: false, json: false, update: true, verbose: false, help: false, error: null };
79
+ const help = argv.includes('--help') || argv.includes('-h');
80
+
81
+ for (let index = 0; index < argv.length; index += 1) {
82
+ const arg = argv[index];
83
+ const [name] = arg.split('=');
84
+ if (VALUE_FLAGS.has(name)) {
85
+ if (!arg.includes('=')) index += 1;
86
+ continue;
87
+ }
88
+ if (!BOOLEAN_FLAGS.has(arg)) {
89
+ result.error = `Unknown option: ${arg}`;
90
+ return result;
91
+ }
92
+ }
93
+
94
+ let parsed;
95
+ try {
96
+ parsed = parseCliArgumentsWithLino({
97
+ argv: argv.filter(arg => arg !== '--help' && arg !== '-h').map(normaliseUpdateFlag),
98
+ commandName: 'hive-models',
99
+ createYargsConfig: createHiveModelsYargsConfig,
100
+ lenv: { enabled: false },
101
+ getenv: { enabled: false },
102
+ });
103
+ } catch (err) {
104
+ result.error = err.message || String(err);
105
+ return result;
106
+ }
107
+
108
+ result.help = help;
109
+ result.refresh = parsed.refresh === true;
110
+ result.details = parsed.details === true;
111
+ result.json = parsed.json === true;
112
+ result.update = parsed.update !== false;
113
+ result.verbose = parsed.verbose === true || parsed.v === true;
114
+
115
+ const requested = []
116
+ .concat(parsed.tool ?? [])
117
+ .flatMap(entry =>
118
+ String(entry)
119
+ .split(',')
120
+ .map(part => part.trim().toLowerCase())
121
+ )
122
+ .filter(Boolean);
123
+ for (const tool of requested) {
124
+ if (!MODEL_CATALOGUE_TOOLS.includes(tool)) {
125
+ result.error = `Unknown tool: ${tool}. Known tools: ${MODEL_CATALOGUE_TOOLS.join(', ')}`;
126
+ return result;
127
+ }
128
+ if (!result.tools.includes(tool)) result.tools.push(tool);
129
+ }
130
+ if (result.tools.length === 0) result.tools = [...MODEL_CATALOGUE_TOOLS];
131
+ return result;
132
+ };
133
+
134
+ /**
135
+ * Top-level orchestrator used by the bin. `deps` is injected so tests can run
136
+ * the whole command without a network, a router, or a package registry.
137
+ */
138
+ export const runHiveModels = async (argv, deps = {}) => {
139
+ const { env = process.env, log = (...args) => console.log(...args), error = (...args) => console.error(...args), loadCatalogue = getMergedModelCatalogue, freshness = ensureAgenticCliFreshness } = deps;
140
+
141
+ const args = parseHiveModelsArgs(argv);
142
+ if (args.help) {
143
+ log(HIVE_MODELS_HELP);
144
+ return 0;
145
+ }
146
+ if (args.error) {
147
+ error(args.error);
148
+ return 1;
149
+ }
150
+
151
+ const debug = args.verbose ? (...parts) => error('[hive-models]', ...parts) : () => {};
152
+
153
+ // R6: refresh the CLIs before answering, so the list describes the binaries
154
+ // the next run will actually use. Best-effort — never fatal.
155
+ const refreshed = await freshness({ tools: args.tools, env, verbose: args.verbose, enabled: args.update, log: async message => debug(message) });
156
+ debug(`cli freshness: ${refreshed.status}${refreshed.reason ? ` (${refreshed.reason})` : ''}`);
157
+ const freshnessLine = describeFreshnessResult(refreshed);
158
+
159
+ const results = {};
160
+ let failures = 0;
161
+ for (const tool of args.tools) {
162
+ try {
163
+ results[tool] = await loadCatalogue({ tool, env, refresh: args.refresh, log: async message => debug(message) });
164
+ } catch (err) {
165
+ failures += 1;
166
+ error(`Could not build the ${tool} catalogue: ${err?.message ?? err}`);
167
+ }
168
+ }
169
+
170
+ if (args.json) {
171
+ log(JSON.stringify({ generatedAt: new Date().toISOString(), cliUpdate: refreshed, tools: results }, null, 2));
172
+ return failures > 0 && Object.keys(results).length === 0 ? 1 : 0;
173
+ }
174
+
175
+ if (freshnessLine) log(freshnessLine);
176
+ const sections = Object.values(results).map(merged => formatModelCatalogueText(merged, { details: args.details, defaultModel: merged.default }));
177
+ log(sections.join('\n\n'));
178
+ return failures > 0 && sections.length === 0 ? 1 : 0;
179
+ };
180
+
181
+ export default { HIVE_MODELS_HELP, parseHiveModelsArgs, runHiveModels };
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * `hive-models` — list every model Hive Mind can drive, merging the models
5
+ * bundled with this installation with the ones the providers are serving right
6
+ * now (issue #2202, R5).
7
+ *
8
+ * Live sources are read only through endpoints that cannot bill a token, and
9
+ * the merged answer is cached for an hour, so running this repeatedly is free.
10
+ *
11
+ * See issue #2202.
12
+ */
13
+
14
+ import { runHiveModels } from './hive-models.lib.mjs';
15
+ import { setupStdioLogInterceptor } from './lib.mjs';
16
+
17
+ setupStdioLogInterceptor();
18
+
19
+ const exitCode = await runHiveModels(process.argv.slice(2));
20
+ process.exit(exitCode);
@@ -590,6 +590,7 @@ en
590
590
  usage "Usage: `/hive <github-url> [options]`"
591
591
  example "Example: `/hive https://github.com/owner/repo`"
592
592
  disabled "*/hive* - ❌ Disabled"
593
+ models "*/models* - List available models, merged from this installation and every live source. Usage: `/models [--tool claude|codex|...] [--details] [--refresh] [--all]`"
593
594
  limits "*/limits* - Show usage limits"
594
595
  version "*/version* - Show bot and runtime versions"
595
596
  language "*/language* `[en|ru|zh|hi]` - Set or show your preferred reply language (in-memory only, per-user)"
@@ -611,7 +612,7 @@ en
611
612
  isolation
612
613
  mode "🔒 *Isolation Mode:* `{{isolationBackend}}` (experimental)"
613
614
  group
614
- note "⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /fix, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /watch, /subscribe and /unsubscribe work in private and group chats."
615
+ note "⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /fix, /hive, /queue, /models, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /watch, /subscribe and /unsubscribe work in private and group chats."
615
616
  common
616
617
  options "🔧 *Common Options:*"
617
618
  model
@@ -590,6 +590,7 @@ hi
590
590
  usage "उपयोग: `/hive <github-url> [options]`"
591
591
  example "उदाहरण: `/hive https://github.com/owner/repo`"
592
592
  disabled "*/hive* - ❌ अक्षम"
593
+ models "*/models* - उपलब्ध models दिखाएँ, जो इस installation और सभी live sources से मिलाकर बनाई गई हैं। उपयोग: `/models [--tool claude|codex|...] [--details] [--refresh] [--all]`"
593
594
  limits "*/limits* - उपयोग सीमाएँ दिखाएँ"
594
595
  version "*/version* - bot और runtime versions दिखाएँ"
595
596
  language "*/language* `[en|ru|zh|hi]` - अपनी पसंदीदा reply language सेट या दिखाएँ (in-memory, per-user)"
@@ -611,7 +612,7 @@ hi
611
612
  isolation
612
613
  mode "🔒 *Isolation Mode:* `{{isolationBackend}}` (experimental)"
613
614
  group
614
- note "⚠️ *नोट:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /fix, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop और /start commands केवल group chats में काम करती हैं। /terminal\\_watch, /watch, /subscribe और /unsubscribe private और group chats में काम करती हैं।"
615
+ note "⚠️ *नोट:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /fix, /hive, /queue, /models, /limits, /version, /accept\\_invites, /merge, /stop और /start commands केवल group chats में काम करती हैं। /terminal\\_watch, /watch, /subscribe और /unsubscribe private और group chats में काम करती हैं।"
615
616
  common
616
617
  options "🔧 *Common Options:*"
617
618
  model
@@ -590,6 +590,7 @@ ru
590
590
  usage "Использование: `/hive <github-url> [options]`"
591
591
  example "Пример: `/hive https://github.com/owner/repo`"
592
592
  disabled "*/hive* - ❌ Отключено"
593
+ models "*/models* - Показать доступные модели, объединённые из этой установки и всех живых источников. Использование: `/models [--tool claude|codex|...] [--details] [--refresh] [--all]`"
593
594
  limits "*/limits* - Показать лимиты использования"
594
595
  version "*/version* - Показать версии бота и среды выполнения"
595
596
  language "*/language* `[en|ru|zh|hi]` - Установить или показать предпочитаемый язык ответов (в памяти, для пользователя)"
@@ -611,7 +612,7 @@ ru
611
612
  isolation
612
613
  mode "🔒 *Режим изоляции:* `{{isolationBackend}}` (экспериментально)"
613
614
  group
614
- note "⚠️ *Замечание:* команды /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /fix, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop и /start работают только в групповых чатах. /terminal\\_watch, /watch, /subscribe и /unsubscribe работают в личных и групповых чатах."
615
+ note "⚠️ *Замечание:* команды /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /fix, /hive, /queue, /models, /limits, /version, /accept\\_invites, /merge, /stop и /start работают только в групповых чатах. /terminal\\_watch, /watch, /subscribe и /unsubscribe работают в личных и групповых чатах."
615
616
  common
616
617
  options "🔧 *Общие опции:*"
617
618
  model
@@ -590,6 +590,7 @@ zh
590
590
  usage "用法:`/hive <github-url> [options]`"
591
591
  example "示例:`/hive https://github.com/owner/repo`"
592
592
  disabled "*/hive* - ❌ 已禁用"
593
+ models "*/models* - 列出可用模型,合并本次安装自带的模型与所有实时来源的模型。用法:`/models [--tool claude|codex|...] [--details] [--refresh] [--all]`"
593
594
  limits "*/limits* - 显示使用限额"
594
595
  version "*/version* - 显示机器人和运行时版本"
595
596
  language "*/language* `[en|ru|zh|hi]` - 设置或显示首选回复语言(内存中,按用户)"
@@ -611,7 +612,7 @@ zh
611
612
  isolation
612
613
  mode "🔒 *隔离模式:* `{{isolationBackend}}`(实验性)"
613
614
  group
614
- note "⚠️ *注意:* /solve、/do、/continue、/claude、/codex、/opencode、/agent、/gemini、/qwen、/task、/split、/fix、/hive、/queue、/limits、/version、/accept\\_invites、/merge、/stop 和 /start 仅在群聊中有效。/terminal\\_watch、/watch、/subscribe 和 /unsubscribe 在私聊和群聊中有效。"
615
+ note "⚠️ *注意:* /solve、/do、/continue、/claude、/codex、/opencode、/agent、/gemini、/qwen、/task、/split、/fix、/hive、/queue、/models、/limits、/version、/accept\\_invites、/merge、/stop 和 /start 仅在群聊中有效。/terminal\\_watch、/watch、/subscribe 和 /unsubscribe 在私聊和群聊中有效。"
615
616
  common
616
617
  options "🔧 *常用选项:*"
617
618
  model