@pellux/goodvibes-tui 0.19.0 → 0.19.1

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
@@ -4,14 +4,51 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.19.1] — 2026-04-17
8
+
9
+ ### Changed
10
+ - Upgraded `@pellux/goodvibes-sdk` from 0.21.1 to 0.21.6 — includes secrets-tier
11
+ `configuredVia` discriminator, HTTP `/api/providers` + `/api/providers/current` (GET/PATCH),
12
+ reactive `model.changed` SSE event, `BUILTIN_LABEL_MAP` brand-accurate provider labels,
13
+ and clean unconfigured-provider errors instead of silent 401 pass-through.
14
+
15
+ ### Added
16
+ - `--provider <id>` and `--model <registryKey>` CLI flags for daemon startup
17
+ (`goodvibes-daemon`) and TUI shell startup (`goodvibes`). Both override
18
+ `~/.goodvibes/tui/settings.json` provider/model values for the session.
19
+ If `--model provider:modelId` format is used, `--provider` is inferred automatically.
20
+ - TUI model picker now populates `configuredVia` per provider and renders it as
21
+ a badge (`[env]`, `[sub]`, `[anon]`) in the provider-browse view, derived from
22
+ env-var presence and active OAuth subscription sessions.
23
+
24
+ ### Fixed
25
+ - **Venice-picking diagnosis**: the silent 401 Venice fallback was caused by the
26
+ pre-0.21.2 SDK passing unconfigured-provider requests through to the upstream API
27
+ unchanged. As of 0.21.2 (included in 0.21.6), `createCompanionProviderAdapter` now
28
+ checks `provider.isConfigured()` before `provider.chat()` and yields a structured
29
+ error immediately. The in-registry fallback to first-selectable-model (which may
30
+ resolve to venice on some installs) only triggers when `getCurrentModel()` cannot
31
+ find the stored `currentModelId` in the model registry — typically an old daemon
32
+ instance with stale config. Restarting the daemon after a settings.json change
33
+ resolves this; the `--model` CLI flag eliminates the need to edit settings.json
34
+ manually. ConfigManager reactivity (live disk-change → registry update) is out of
35
+ scope: the PATCH `/api/providers/current` route is the correct live-switch path
36
+ (calls `setCurrentModel()` + `configManager.set()` atomically).
37
+
38
+
7
39
  ## [0.19.0] — 2026-04-18
8
40
 
9
41
  ### Changed
10
42
  - Upgraded `@pellux/goodvibes-sdk` from 0.19.6 to 0.21.1 (soak-period release).
11
- TUI adaptation required: regenerated `docs/foundation-artifacts/operator-contract.json`
12
- to match the updated `buildOperatorContract()` output in the new SDK version.
13
- `peer-contract.json`, `knowledge-graphql.graphql`, and `knowledge-store.sql` were
14
- unchanged by this SDK bump.
43
+ TUI adaptations required:
44
+ 1. `docs/foundation-artifacts/operator-contract.json` regenerated to match
45
+ updated `buildOperatorContract()` output (`peer-contract.json`, knowledge
46
+ artifacts unchanged).
47
+ 2. `scripts/perf-check.ts` — `platform/runtime/perf/index` barrel removed;
48
+ `createPerfMonitor()` factory removed; migrated to `new PerfMonitor()` with
49
+ imports split to `perf/monitor` and `perf/reporter` sub-paths.
50
+ 3. `scripts/eval-gate.ts` — `platform/runtime/eval/index` barrel removed;
51
+ imports split to `eval/baseline`, `eval/format`, and `eval/scorecard`.
15
52
 
16
53
  ### Added
17
54
  - Wave B panel migration: migrated 5 panels (knowledge, marketplace, memory,
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.19.0-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.19.1-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
@@ -3,7 +3,7 @@
3
3
  "product": {
4
4
  "id": "goodvibes",
5
5
  "surface": "operator",
6
- "version": "0.21.1"
6
+ "version": "0.21.6"
7
7
  },
8
8
  "auth": {
9
9
  "modes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -89,7 +89,7 @@
89
89
  "@anthropic-ai/vertex-sdk": "^0.16.0",
90
90
  "@ast-grep/napi": "^0.42.0",
91
91
  "@aws/bedrock-token-generator": "^1.1.0",
92
- "@pellux/goodvibes-sdk": "0.21.1",
92
+ "@pellux/goodvibes-sdk": "0.21.6",
93
93
  "bash-language-server": "^5.6.0",
94
94
  "fuse.js": "^7.1.0",
95
95
  "graphql": "^16.13.2",
package/src/daemon/cli.ts CHANGED
@@ -22,6 +22,49 @@ type DaemonCliOwnership = {
22
22
  readonly homeDirectory: string;
23
23
  };
24
24
 
25
+ // ---------------------------------------------------------------------------
26
+ // CLI flag parsing
27
+ // ---------------------------------------------------------------------------
28
+
29
+ type DaemonCliFlags = {
30
+ readonly provider: string | undefined;
31
+ readonly model: string | undefined;
32
+ };
33
+
34
+ function parseDaemonCliFlags(argv: readonly string[]): DaemonCliFlags {
35
+ let provider: string | undefined;
36
+ let model: string | undefined;
37
+
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const arg = argv[i];
40
+ if (arg === '--help' || arg === '-h') {
41
+ // eslint-disable-next-line no-console
42
+ console.log([
43
+ 'Usage: goodvibes-daemon [options]',
44
+ '',
45
+ 'Options:',
46
+ ' --provider <id> Override the provider from settings.json at startup',
47
+ ' --model <registryKey> Override the model from settings.json at startup',
48
+ ' Format: provider:modelId (e.g. inception:mercury-2)',
49
+ ' If provider:modelId format is used, --provider is inferred',
50
+ ' --help, -h Show this help message',
51
+ ].join('\n'));
52
+ process.exit(0);
53
+ }
54
+ if (arg === '--provider' && argv[i + 1] !== undefined) {
55
+ provider = argv[++i];
56
+ } else if (arg === '--model' && argv[i + 1] !== undefined) {
57
+ model = argv[++i];
58
+ // Infer provider from registryKey format (provider:modelId) if --provider not given
59
+ if (typeof model === 'string' && model.includes(':') && provider === undefined) {
60
+ provider = model.split(':')[0];
61
+ }
62
+ }
63
+ }
64
+
65
+ return { provider, model };
66
+ }
67
+
25
68
  type DaemonCliTokens = {
26
69
  readonly daemonToken: string | undefined;
27
70
  readonly httpToken: string | undefined;
@@ -72,6 +115,17 @@ async function main(): Promise<void> {
72
115
  const { workingDirectory: workingDir, homeDirectory } = resolveDaemonCliOwnership();
73
116
  const config = new ConfigManager({ workingDir, homeDir: homeDirectory, surfaceRoot: 'tui' });
74
117
  new GlobalNetworkTransportInstaller().install(config);
118
+
119
+ // Apply CLI flags — override settings.json before the provider registry is constructed
120
+ const cliFlags = parseDaemonCliFlags(process.argv.slice(2));
121
+ if (cliFlags.provider !== undefined) {
122
+ config.set('provider.provider', cliFlags.provider);
123
+ logger.info('daemon: --provider flag applied', { provider: cliFlags.provider });
124
+ }
125
+ if (cliFlags.model !== undefined) {
126
+ config.set('provider.model', cliFlags.model);
127
+ logger.info('daemon: --model flag applied', { model: cliFlags.model });
128
+ }
75
129
  const runtimeBus = new RuntimeEventBus();
76
130
  const runtimeStore = createRuntimeStore();
77
131
  const runtimeServices = createRuntimeServices({
@@ -99,6 +99,8 @@ export interface PickerItem {
99
99
  isFree?: boolean;
100
100
  /** True when this provider item has a configured API key. */
101
101
  isConfigured?: boolean;
102
+ /** How the provider is configured — shown as a badge in provider mode. */
103
+ configuredVia?: 'env' | 'secrets' | 'subscription' | 'anonymous';
102
104
  }
103
105
 
104
106
  /** Provider IDs treated as "Popular" in the provider picker. */
@@ -166,6 +168,8 @@ export class ModelPickerModal {
166
168
  public availableOnly = true;
167
169
  /** Set of provider names that have a configured key (used for availableOnly filter). */
168
170
  public configuredProviders: Set<string> = new Set();
171
+ /** How each provider is configured — drives badge display in provider mode. */
172
+ public configuredViaMap: Map<string, 'env' | 'secrets' | 'subscription' | 'anonymous'> = new Map();
169
173
  /** IDs of pinned/favorite models — shown at top of list. */
170
174
  public pinnedIds: Set<string> = new Set();
171
175
  /** IDs of recently used models — shown after pinned, before the rest. */
@@ -612,13 +616,13 @@ export class ModelPickerModal {
612
616
  if (filteredPopular.length > 0) {
613
617
  providerItems.push({ id: '__header__popular', label: 'Popular', isGroupHeader: true });
614
618
  for (const p of filteredPopular) {
615
- providerItems.push({ id: p, label: p, isConfigured: this.configuredProviders.has(p) });
619
+ providerItems.push({ id: p, label: p, isConfigured: this.configuredProviders.has(p), configuredVia: this.configuredViaMap.get(p) });
616
620
  }
617
621
  }
618
622
  if (filteredAll.length > 0) {
619
623
  providerItems.push({ id: '__header__all', label: 'All Providers', isGroupHeader: true });
620
624
  for (const p of filteredAll) {
621
- providerItems.push({ id: p, label: p, isConfigured: this.configuredProviders.has(p) });
625
+ providerItems.push({ id: p, label: p, isConfigured: this.configuredProviders.has(p), configuredVia: this.configuredViaMap.get(p) });
622
626
  }
623
627
  }
624
628
 
package/src/main.ts CHANGED
@@ -80,6 +80,49 @@ function resolveShellEntrypointOwnership(): ShellEntrypointOwnership {
80
80
  };
81
81
  }
82
82
 
83
+ // ---------------------------------------------------------------------------
84
+ // CLI flag parsing (TUI shell entry point)
85
+ // ---------------------------------------------------------------------------
86
+
87
+ type TuiCliFlags = {
88
+ readonly provider: string | undefined;
89
+ readonly model: string | undefined;
90
+ };
91
+
92
+ function parseTuiCliFlags(argv: readonly string[]): TuiCliFlags {
93
+ let provider: string | undefined;
94
+ let model: string | undefined;
95
+
96
+ for (let i = 0; i < argv.length; i++) {
97
+ const arg = argv[i];
98
+ if (arg === '--help' || arg === '-h') {
99
+ // eslint-disable-next-line no-console
100
+ console.log([
101
+ 'Usage: goodvibes [options]',
102
+ '',
103
+ 'Options:',
104
+ ' --provider <id> Override the provider from settings.json at startup',
105
+ ' --model <registryKey> Override the model from settings.json at startup',
106
+ ' Format: provider:modelId (e.g. inception:mercury-2)',
107
+ ' If provider:modelId format is used, --provider is inferred',
108
+ ' --help, -h Show this help message',
109
+ ].join('\n'));
110
+ process.exit(0);
111
+ }
112
+ if (arg === '--provider' && argv[i + 1] !== undefined) {
113
+ provider = argv[++i];
114
+ } else if (arg === '--model' && argv[i + 1] !== undefined) {
115
+ model = argv[++i];
116
+ // Infer provider from registryKey format (provider:modelId) if --provider not given
117
+ if (typeof model === 'string' && model.includes(':') && provider === undefined) {
118
+ provider = model.split(':')[0];
119
+ }
120
+ }
121
+ }
122
+
123
+ return { provider, model };
124
+ }
125
+
83
126
  async function main() {
84
127
  const stdout = process.stdout;
85
128
  const stdin = process.stdin;
@@ -95,6 +138,15 @@ async function main() {
95
138
  });
96
139
  new GlobalNetworkTransportInstaller().install(configManager);
97
140
 
141
+ // Apply CLI flags — override settings.json before the provider registry is constructed
142
+ const cliFlags = parseTuiCliFlags(process.argv.slice(2));
143
+ if (cliFlags.provider !== undefined) {
144
+ configManager.set('provider.provider', cliFlags.provider);
145
+ }
146
+ if (cliFlags.model !== undefined) {
147
+ configManager.set('provider.model', cliFlags.model);
148
+ }
149
+
98
150
  // ── Bootstrap all runtime subsystems ─────────────────────────────────────
99
151
  // bootstrapRuntime initializes all subsystems in dependency order and returns
100
152
  // a fully-wired BootstrapContext. main.ts owns terminal setup, the render loop,
@@ -294,12 +294,19 @@ export function renderModelPickerOverlay(
294
294
  const isSelected = selectableIdx === picker.selectedIndex;
295
295
  const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
296
296
  const checkmark = item.isConfigured ? '✓ ' : ' ';
297
- const labelW = contentW - 2 - 2; // indicator(2) + checkmark(2)
297
+ // configuredVia badge: right-aligned short label (env/sub/anon)
298
+ const viaBadge = item.configuredVia === 'env' ? ' [env]'
299
+ : item.configuredVia === 'secrets' ? ' [key]'
300
+ : item.configuredVia === 'subscription' ? ' [sub]'
301
+ : item.configuredVia === 'anonymous' ? ' [anon]'
302
+ : '';
303
+ const badgeW = viaBadge.length;
304
+ const labelW = contentW - 2 - 2 - badgeW; // indicator(2) + checkmark(2) + badge
298
305
  const labelStr = item.label.length > labelW
299
306
  ? item.label.slice(0, labelW - 3) + '...'
300
307
  : item.label.padEnd(labelW);
301
308
  const row = createOverlayContentLine(width, layout, borderFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg);
302
- const rowText = indicator + checkmark + labelStr;
309
+ const rowText = indicator + checkmark + labelStr + viaBadge;
303
310
  putRowText(row, layout.margin + 2, contentW, fitDisplay(truncateDisplay(rowText, contentW), contentW), isSelected ? titleFg : bodyFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg, isSelected);
304
311
  lines.push(row);
305
312
  }
@@ -27,6 +27,42 @@ type WireShellUiOpenersOptions = {
27
27
  render: () => void;
28
28
  };
29
29
 
30
+ /**
31
+ * Derive the configuredVia tier for a provider.
32
+ * Checks env-vars first (env tier), then falls back to subscription (from configuredIds
33
+ * that aren't env-keyed). Returns undefined when not configured.
34
+ */
35
+ function deriveConfiguredVia(
36
+ providerId: string,
37
+ configuredIds: Set<string>,
38
+ subscriptionManager: SubscriptionManager,
39
+ ): 'env' | 'secrets' | 'subscription' | 'anonymous' | undefined {
40
+ if (!configuredIds.has(providerId)) return undefined;
41
+
42
+ // Check if a subscription session is active for this provider
43
+ const subs = subscriptionManager.list();
44
+ if (subs.some((s) => s.provider === providerId)) return 'subscription';
45
+
46
+ // Assume env-var backed (anonymous providers don't appear in configuredIds)
47
+ return 'env';
48
+ }
49
+
50
+ /**
51
+ * Build a configuredViaMap for the given provider list.
52
+ */
53
+ function buildConfiguredViaMap(
54
+ providers: string[],
55
+ configuredIds: Set<string>,
56
+ subscriptionManager: SubscriptionManager,
57
+ ): Map<string, 'env' | 'secrets' | 'subscription' | 'anonymous'> {
58
+ const map = new Map<string, 'env' | 'secrets' | 'subscription' | 'anonymous'>();
59
+ for (const p of providers) {
60
+ const via = deriveConfiguredVia(p, configuredIds, subscriptionManager);
61
+ if (via !== undefined) map.set(p, via);
62
+ }
63
+ return map;
64
+ }
65
+
30
66
  export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
31
67
  const {
32
68
  commandContext,
@@ -47,7 +83,10 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
47
83
 
48
84
  commandContext.openModelPicker = () => {
49
85
  const models = providerRegistry.getSelectableModels();
50
- input.modelPicker.configuredProviders = new Set(getConfiguredProviderIds());
86
+ const configuredIds = new Set(getConfiguredProviderIds());
87
+ input.modelPicker.configuredProviders = configuredIds;
88
+ const providerIds = [...new Set(models.map((m) => m.provider))];
89
+ input.modelPicker.configuredViaMap = buildConfiguredViaMap(providerIds, configuredIds, subscriptionManager);
51
90
  void getPinned().then((pinned) => {
52
91
  input.modelPicker.pinnedIds = new Set(pinned);
53
92
  });
@@ -59,7 +98,9 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
59
98
 
60
99
  commandContext.openProviderPicker = () => {
61
100
  const providers = [...new Set(providerRegistry.listModels().map((model) => model.provider))];
62
- input.modelPicker.configuredProviders = new Set(getConfiguredProviderIds());
101
+ const configuredIds = new Set(getConfiguredProviderIds());
102
+ input.modelPicker.configuredProviders = configuredIds;
103
+ input.modelPicker.configuredViaMap = buildConfiguredViaMap(providers, configuredIds, subscriptionManager);
63
104
  input.modalOpened('modelPicker');
64
105
  input.modelPicker.openProviders(providers, runtime.provider);
65
106
  render();
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.19.0';
9
+ let _version = '0.19.1';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;