@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 +11 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/config/credential-availability.ts +122 -0
- package/src/config/index.ts +15 -0
- package/src/runtime/bootstrap-core.ts +13 -13
- package/src/runtime/memory-fold.ts +29 -0
- package/src/version.ts +1 -1
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
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](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.
|
|
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
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -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
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
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
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
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.
|
|
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;
|