@pellux/goodvibes-tui 1.9.0 → 1.9.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,6 +4,17 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.9.1] — 2026-07-06
8
+
9
+ Follow-up patch that completes the W6-C1 credential-status adoption for the TUI. The 1.9.0 release pinned SDK 1.0.0 and unified memory (C2) but left the C1 client-side credential-status read for a follow-up; this release lands it. Ships against @pellux/goodvibes-sdk 1.0.0.
10
+
11
+ ### Credential-status read as a daemon client (W6-C1)
12
+ - When the TUI acts as a client of an adopted external daemon (`HostServiceStatus` mode `external`), provider/model/secret STATUS is read from that daemon's `credentials.get` wire method instead of the TUI's own surfaceRoot store. The read carries only status metadata (key / configured / usable / source / secure) — never a secret value, mirroring the SDK's status-only wire contract. The env-only API-key posture and all secret value-reads that provider auth needs stay local and are untouched; only STATUS reads use the daemon path.
13
+ - Honest degraded state, mirroring goodvibes-webui v1.0.1 `deriveCredentialAvailability` exactly: a 503 `CREDENTIAL_STORE_UNAVAILABLE` (matched by machine code), a `METHOD_NOT_FOUND` from an older daemon, or any transport failure resolves to a reason-carrying `available: false` — never a fabricated "configured", never a stale confident value. The TUI-hosted daemon continues to serve `credentials.get` automatically from its existing secrets manager (SDK auto-wiring), so no serve-side change was needed.
14
+
15
+ ### Memory unification follow-up (W6-C2)
16
+ - The legacy per-project TUI memory store now actually folds into the home-scoped canonical store once at boot, after the canonical store initializes — making good on the behavior 1.9.0 described. The fold is id-keyed and idempotent (a re-run imports nothing new and never deletes the legacy file) and is strictly non-fatal: a fold error degrades to a warning and the canonical store serves on. The home-scoped store deliberately does not re-root with the working directory; only the code index and project index do.
17
+
7
18
  ## [1.7.0] — 2026-07-04
8
19
 
9
20
  The evolution release: six batched development cycles (internal 1.2.0–1.6.0, which were never published) land together as one public version. The through-line is a product that reports the truth about itself — reversible edits you can actually undo, a live fleet you can watch and steer, orchestration that runs the plan it showed you, memory that demonstrably enters the turns it claims to, and a panel surface cut down to the consoles that earn their place. Ships against @pellux/goodvibes-sdk 0.38.0.
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-1.9.0-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-1.9.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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "1.9.0",
3
+ "version": "1.9.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",
@@ -0,0 +1,122 @@
1
+ /**
2
+ * credential-availability.ts — W6-C1 client-side credential-status read.
3
+ *
4
+ * When the TUI acts as a CLIENT of an adopted external daemon (the host-service
5
+ * `mode === 'external'` topology — see runtime/bootstrap.ts), provider/model/secret
6
+ * STATUS is read from that daemon's `credentials.get` wire method rather than from
7
+ * the TUI's own surfaceRoot ('tui') store. This module folds a `credentials.get`
8
+ * outcome into an honest availability value, mirroring the goodvibes-webui v1.0.1
9
+ * `deriveCredentialAvailability` contract exactly:
10
+ *
11
+ * - a 503 CREDENTIAL_STORE_UNAVAILABLE (matched by machine code), a METHOD_NOT_FOUND
12
+ * from an older daemon, or ANY transport failure -> { available: false, reason }
13
+ * — an honest, reason-carrying "unavailable" state.
14
+ * - NEVER a fabricated "configured"; NEVER a secret byte. Only the boolean status
15
+ * metadata surface (key / configured / usable / source / secure) is carried.
16
+ *
17
+ * STATUS ONLY moves to the daemon path. Secret RESOLUTION — the value-reads provider
18
+ * auth needs, plus the env-only API-key posture — stays local and is untouched by
19
+ * this module. The daemon's `credentials.get` never returns raw key bytes over the
20
+ * wire (see the SDK decision record 2026-07-06-config-sharing-shared-tier-and-secret-read),
21
+ * so no plaintext can reach a caller through this path by construction.
22
+ */
23
+
24
+ /** One credential's status metadata from the daemon's shared store — never bytes. */
25
+ export interface CredentialStatusEntry {
26
+ readonly key: string;
27
+ readonly configured: boolean;
28
+ readonly usable: boolean;
29
+ readonly source?: string;
30
+ readonly secure?: boolean;
31
+ }
32
+
33
+ /** Honest availability: either a status list, or a reason we could not read it. */
34
+ export type CredentialAvailability =
35
+ | { readonly available: true; readonly credentials: readonly CredentialStatusEntry[] }
36
+ | { readonly available: false; readonly reason: string };
37
+
38
+ /** A `credentials.get` invocation outcome — success value OR a thrown error. */
39
+ export type CredentialStatusOutcome =
40
+ | { readonly ok: true; readonly value: unknown }
41
+ | { readonly ok: false; readonly error: unknown };
42
+
43
+ /** Credential-read posture: read the daemon's shared store, or the local own store. */
44
+ export type CredentialReadMode = 'host' | 'client';
45
+
46
+ function asRecord(value: unknown): Record<string, unknown> | null {
47
+ return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : null;
48
+ }
49
+
50
+ function readString(record: Record<string, unknown> | null, key: string): string {
51
+ if (!record) return '';
52
+ const value = record[key];
53
+ return typeof value === 'string' ? value : '';
54
+ }
55
+
56
+ /**
57
+ * Map the authoritative host-service mode string to the credential-read posture.
58
+ * Only 'external' (a separately-running daemon this TUI adopted) reads credential
59
+ * STATUS over the wire; every other mode ('embedded'/'disabled'/'blocked'/
60
+ * 'incompatible'/'unavailable') is the local host reading its own store.
61
+ */
62
+ export function credentialReadModeFromHostMode(hostMode: string): CredentialReadMode {
63
+ return hostMode === 'external' ? 'client' : 'host';
64
+ }
65
+
66
+ /**
67
+ * Fold a `credentials.get` outcome into an honest availability value. Mirrors
68
+ * goodvibes-webui v1.0.1 `deriveCredentialAvailability`: any failure becomes a
69
+ * reason-carrying `available: false`; a malformed success (no credentials array)
70
+ * is treated as unavailable rather than fabricated as configured; on success only
71
+ * the boolean status surface is carried, never a secret value.
72
+ */
73
+ export function deriveCredentialAvailability(outcome: CredentialStatusOutcome): CredentialAvailability {
74
+ if (!outcome.ok) {
75
+ const err = asRecord(outcome.error);
76
+ const code = readString(err, 'code');
77
+ if (code === 'CREDENTIAL_STORE_UNAVAILABLE') {
78
+ return { available: false, reason: 'The daemon has no shared credential store wired.' };
79
+ }
80
+ if (code === 'METHOD_NOT_FOUND' || code === 'NOT_INVOKABLE') {
81
+ return { available: false, reason: 'This daemon does not serve credential status yet.' };
82
+ }
83
+ return { available: false, reason: 'Credential status unavailable right now.' };
84
+ }
85
+ const value = asRecord(outcome.value);
86
+ const raw = value?.credentials;
87
+ if (!Array.isArray(raw)) return { available: false, reason: 'Credential status unavailable right now.' };
88
+ const credentials: CredentialStatusEntry[] = [];
89
+ for (const item of raw) {
90
+ const rec = asRecord(item);
91
+ const key = readString(rec, 'key');
92
+ if (!rec || !key) continue;
93
+ credentials.push({
94
+ key,
95
+ configured: rec.configured === true,
96
+ usable: rec.usable === true,
97
+ source: readString(rec, 'source') || undefined,
98
+ secure: rec.secure === true ? true : rec.secure === false ? false : undefined,
99
+ });
100
+ }
101
+ return { available: true, credentials };
102
+ }
103
+
104
+ /**
105
+ * Client-side credential-status read. Invokes the adopted external daemon's
106
+ * `credentials.get` and folds success OR any thrown transport/daemon error into an
107
+ * honest availability. The daemon's `credentials.get` transport error carries the
108
+ * machine `code` at the top level (e.g. an HttpStatusError whose body supplied
109
+ * `CREDENTIAL_STORE_UNAVAILABLE` / `METHOD_NOT_FOUND`), which `deriveCredentialAvailability`
110
+ * classifies. Only the boolean status surface is returned; a raw secret value can
111
+ * never reach a caller through this path.
112
+ */
113
+ export async function readClientCredentialStatus(
114
+ invokeCredentialsGet: () => Promise<unknown>,
115
+ ): Promise<CredentialAvailability> {
116
+ try {
117
+ const value = await invokeCredentialsGet();
118
+ return deriveCredentialAvailability({ ok: true, value });
119
+ } catch (error) {
120
+ return deriveCredentialAvailability({ ok: false, error });
121
+ }
122
+ }
@@ -55,3 +55,18 @@ export function getConfiguredSystemPrompt(configManager: Pick<ConfigManager, 'ge
55
55
  }
56
56
 
57
57
  export { getConfiguredApiKeys, resolveApiKeys } from '@pellux/goodvibes-sdk/platform/config';
58
+
59
+ // W6-C1: client-side credential-status read (honest-degrade, status-only). When the
60
+ // TUI acts as a client of an adopted external daemon it reads provider/model/secret
61
+ // STATUS from that daemon's `credentials.get`, never from its own surfaceRoot store.
62
+ export {
63
+ credentialReadModeFromHostMode,
64
+ deriveCredentialAvailability,
65
+ readClientCredentialStatus,
66
+ } from './credential-availability.ts';
67
+ export type {
68
+ CredentialAvailability,
69
+ CredentialReadMode,
70
+ CredentialStatusEntry,
71
+ CredentialStatusOutcome,
72
+ } from './credential-availability.ts';
@@ -27,6 +27,7 @@ import { loadBootstrapSystemPrompt, syncConfiguredServices } from '@/runtime/ind
27
27
  import { registerBootstrapHookBridge } from '@/runtime/index.ts';
28
28
  import { registerBootstrapRuntimeEvents } from '@/runtime/index.ts';
29
29
  import { createRuntimeServices, type RuntimeServices } from './services.ts';
30
+ import { runBootMemoryFold } from './memory-fold.ts';
30
31
  import { setPricingSource } from '../export/cost-utils.ts';
31
32
  import { createUiRuntimeServices, type UiRuntimeServices } from './ui-services.ts';
32
33
  import { join } from 'node:path';
@@ -407,24 +408,23 @@ export async function initializeBootstrapCore(
407
408
  void memoryStore.save();
408
409
  memoryStore.close();
409
410
  });
411
+ // W6-C2 (E6): fold this project's legacy per-project TUI memory into the home-scoped canonical store, ONCE, AFTER init(). Idempotent and non-fatal.
412
+ await runBootMemoryFold(memoryStore, services.memoryEmbeddingRegistry, workingDir, logger);
410
413
 
411
414
  const renderRequestRef = { value: (): void => {} };
412
- // R1: Coalescing render scheduler — collapses N requestRender() calls into 1
413
- // and enforces a 16ms minimum interval to cap repaints at ~60fps.
414
- //
415
- // renderScheduled stays set for the ENTIRE window (until run() executes), so
416
- // requestRender() calls that arrive on later event-loop ticks within the same
417
- // 16ms window are coalesced into the one already-pending tail render instead
418
- // of each queuing their own setTimeout. (The streaming hot path drives its own
419
- // direct repaints and does not flow through this scheduler.)
415
+ // R1: Coalescing render scheduler — collapses N requestRender() calls into 1 and
416
+ // enforces a 16ms minimum interval to cap repaints at ~60fps. renderScheduled stays
417
+ // set for the ENTIRE window (until run() executes), so requestRender() calls arriving
418
+ // on later event-loop ticks within the same 16ms window coalesce into the one
419
+ // already-pending tail render instead of each queuing their own setTimeout. (The
420
+ // streaming hot path drives its own direct repaints and skips this scheduler.)
420
421
  let renderScheduled = false;
421
422
  let lastRenderTime = 0;
422
423
  const RENDER_INTERVAL_MS = 16;
423
- // run() performs the actual render. It clears renderScheduled FIRST — even if
424
- // the render callback throws — otherwise a single render exception would wedge
425
- // the entire TUI (no future requestRender() call would schedule anything). The
426
- // renderer is expected to surface failures via its own error path; we log at
427
- // error so the next requestRender() can still reschedule.
424
+ // run() performs the actual render. It clears renderScheduled FIRST — even if the
425
+ // render callback throws — otherwise a single render exception would wedge the entire
426
+ // TUI (no future requestRender() would schedule anything); we log at error so the
427
+ // next requestRender() can still reschedule.
428
428
  const run = (): void => {
429
429
  renderScheduled = false;
430
430
  lastRenderTime = Date.now();
@@ -1,6 +1,7 @@
1
1
  import { join } from 'node:path';
2
2
  import {
3
3
  foldMemoryStores,
4
+ formatMemoryFoldReport,
4
5
  type LegacyMemorySource,
5
6
  type MemoryEmbeddingProviderRegistry,
6
7
  type MemoryFoldReport,
@@ -24,3 +25,31 @@ export async function foldTuiLegacyMemory(
24
25
  ];
25
26
  return foldMemoryStores(memoryStore, sources, { embeddingRegistry: memoryEmbeddingRegistry });
26
27
  }
28
+
29
+ /** A minimal boot logger seam (matches the SDK logger's info/warn shape). */
30
+ export interface BootFoldLogger {
31
+ info(message: string, meta?: unknown): void;
32
+ warn(message: string, meta?: unknown): void;
33
+ }
34
+
35
+ /**
36
+ * Run the legacy TUI memory fold at boot and surface the report. Never blocks or
37
+ * fails boot: a fold error degrades to a warn and the canonical store serves on.
38
+ * Only logs the report when something actually moved (or a source failed), so a
39
+ * clean boot stays quiet.
40
+ */
41
+ export async function runBootMemoryFold(
42
+ memoryStore: MemoryStore,
43
+ memoryEmbeddingRegistry: MemoryEmbeddingProviderRegistry,
44
+ workingDirectory: string,
45
+ log: BootFoldLogger,
46
+ ): Promise<void> {
47
+ try {
48
+ const report = await foldTuiLegacyMemory(memoryStore, memoryEmbeddingRegistry, workingDirectory);
49
+ if (report.totalImported > 0 || report.failedSources.length > 0) {
50
+ log.info(`[bootstrap] memory fold: ${formatMemoryFoldReport(report)}`);
51
+ }
52
+ } catch (err) {
53
+ log.warn('memory fold at bootstrap failed (non-fatal; canonical store unaffected)', { err });
54
+ }
55
+ }
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 = '1.9.0';
9
+ let _version = '1.9.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;