@pellux/goodvibes-agent 0.1.77 → 0.1.79

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
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.79 - 2026-06-01
6
+
7
+ - Expanded the Voice & Media workspace into a provider readiness matrix covering selected TTS readiness, missing secret key names, media understanding/generation posture, and browser-tool state.
8
+ - Kept voice, browser, and generated media side effects explicit: the workspace renders setup state and next steps only, never secret values or hidden runtime actions.
9
+
10
+ ## 0.1.78 - 2026-06-01
11
+
12
+ - Expanded the Agent channel workspace into a concrete readiness matrix with setup state, missing runtime config keys, default-target posture, and safe next steps for each externally owned channel.
13
+ - Kept channel setup read-only inside Agent: no runtime lifecycle ownership, no hidden sends, and no secret values rendered.
14
+
5
15
  ## 0.1.77 - 2026-06-01
6
16
 
7
17
  - Added a first-class Agent profile creation form inside the fullscreen operator workspace instead of dispatching a placeholder command template.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.77",
3
+ "version": "0.1.79",
4
4
  "private": false,
5
5
  "description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
6
6
  "type": "module",
@@ -7,11 +7,17 @@ export interface AgentWorkspaceChannelStatus {
7
7
  readonly label: string;
8
8
  readonly enabled: boolean;
9
9
  readonly ready: boolean;
10
+ readonly requiredKeys: readonly string[];
11
+ readonly missingRequiredKeys: readonly string[];
10
12
  readonly missingConfigCount: number;
13
+ readonly defaultTargetKeys: readonly string[];
14
+ readonly configuredDefaultTargetKeys: readonly string[];
11
15
  readonly defaultTarget: 'configured' | 'missing' | 'not-required';
12
16
  readonly delivery: 'disabled' | 'blocked' | 'explicit-target' | 'default-ready';
13
17
  readonly risk: AgentWorkspaceChannelRisk;
14
18
  readonly riskLabel: string;
19
+ readonly setupState: 'disabled' | 'needs-config' | 'needs-target' | 'ready';
20
+ readonly nextStep: string;
15
21
  }
16
22
 
17
23
  interface AgentWorkspaceConfigReader {
@@ -182,10 +188,12 @@ function hasConfigValue(context: CommandContext, key: string): boolean {
182
188
 
183
189
  function buildChannelStatus(context: CommandContext, spec: AgentWorkspaceChannelSpec): AgentWorkspaceChannelStatus {
184
190
  const enabled = readConfigBoolean(context, spec.enabledKey, false);
185
- const missingConfigCount = spec.requiredKeys.filter((key) => !hasConfigValue(context, key)).length;
191
+ const missingRequiredKeys = spec.requiredKeys.filter((key) => !hasConfigValue(context, key));
192
+ const configuredDefaultTargetKeys = spec.defaultTargetKeys.filter((key) => hasConfigValue(context, key));
193
+ const missingConfigCount = missingRequiredKeys.length;
186
194
  const defaultTarget = spec.defaultTargetKeys.length === 0
187
195
  ? 'not-required'
188
- : spec.defaultTargetKeys.some((key) => hasConfigValue(context, key))
196
+ : configuredDefaultTargetKeys.length > 0
189
197
  ? 'configured'
190
198
  : 'missing';
191
199
  const ready = enabled && missingConfigCount === 0;
@@ -196,16 +204,36 @@ function buildChannelStatus(context: CommandContext, spec: AgentWorkspaceChannel
196
204
  : defaultTarget === 'configured'
197
205
  ? 'default-ready'
198
206
  : 'explicit-target';
207
+ const setupState = !enabled
208
+ ? 'disabled'
209
+ : missingConfigCount > 0
210
+ ? 'needs-config'
211
+ : defaultTarget === 'missing'
212
+ ? 'needs-target'
213
+ : 'ready';
214
+ const nextStep = setupState === 'disabled'
215
+ ? `Enable ${spec.label} in the owning GoodVibes runtime before Agent can use it.`
216
+ : setupState === 'needs-config'
217
+ ? `Configure ${missingRequiredKeys.join(', ')} in the owning runtime or secret manager.`
218
+ : setupState === 'needs-target'
219
+ ? `Provide an explicit delivery target per send, or configure one of ${spec.defaultTargetKeys.join(', ')}.`
220
+ : `Use explicit user action or runtime policy to send through ${spec.label}.`;
199
221
  return {
200
222
  id: spec.id,
201
223
  label: spec.label,
202
224
  enabled,
203
225
  ready,
226
+ requiredKeys: spec.requiredKeys,
227
+ missingRequiredKeys,
204
228
  missingConfigCount,
229
+ defaultTargetKeys: spec.defaultTargetKeys,
230
+ configuredDefaultTargetKeys,
205
231
  defaultTarget,
206
232
  delivery,
207
233
  risk: spec.risk,
208
234
  riskLabel: spec.riskLabel,
235
+ setupState,
236
+ nextStep,
209
237
  };
210
238
  }
211
239
 
@@ -6,6 +6,7 @@ import { AgentSkillRegistry, type AgentSkillRecord } from '../agent/skill-regist
6
6
  import { getAgentRuntimeProfilesRoot, listAgentRuntimeProfiles, listAgentRuntimeProfileTemplates } from '../agent/runtime-profile.ts';
7
7
  import { buildAgentWorkspaceChannels } from './agent-workspace-channels.ts';
8
8
  import { buildAgentWorkspaceSetupChecklist } from './agent-workspace-setup.ts';
9
+ import { buildAgentWorkspaceVoiceMediaReadiness, type AgentWorkspaceVoiceMediaProviderDescriptor } from './agent-workspace-voice-media.ts';
9
10
  import type {
10
11
  AgentWorkspaceLocalLibraryItem,
11
12
  AgentWorkspaceRuntimeProfileItem,
@@ -217,6 +218,16 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
217
218
  return [];
218
219
  }
219
220
  })();
221
+ const voiceProviderDescriptors: readonly AgentWorkspaceVoiceMediaProviderDescriptor[] = voiceProviders.map((provider) => ({
222
+ id: provider.id,
223
+ label: provider.label,
224
+ capabilities: provider.capabilities,
225
+ }));
226
+ const mediaProviderDescriptors: readonly AgentWorkspaceVoiceMediaProviderDescriptor[] = mediaProviders.map((provider) => ({
227
+ id: provider.id,
228
+ label: provider.label,
229
+ capabilities: provider.capabilities,
230
+ }));
220
231
  const warnings: string[] = [];
221
232
  if (provider === 'unknown' || model === 'unknown') warnings.push('Provider/model unavailable in this runtime context.');
222
233
  if (!context.executeCommand) warnings.push('Command dispatch is unavailable; workspace actions will show guidance only.');
@@ -226,6 +237,11 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
226
237
  const ttsLlmModel = readConfigString(context, 'tts.llmModel', '');
227
238
  const daemonBaseUrl = `http://${host}:${port}`;
228
239
  const channels = buildAgentWorkspaceChannels(context);
240
+ const voiceMediaReadiness = buildAgentWorkspaceVoiceMediaReadiness({
241
+ context,
242
+ voiceProviders: voiceProviderDescriptors,
243
+ mediaProviders: mediaProviderDescriptors,
244
+ });
229
245
  const setupChecklist = buildAgentWorkspaceSetupChecklist({
230
246
  provider,
231
247
  model,
@@ -278,6 +294,7 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
278
294
  mediaProviderCount: mediaProviders.length,
279
295
  mediaUnderstandingProviderCount: mediaProviders.filter((entry) => entry.capabilities.includes('understand')).length,
280
296
  mediaGenerationProviderCount: mediaProviders.filter((entry) => entry.capabilities.includes('generate')).length,
297
+ voiceMediaReadiness,
281
298
  browserSurfaceEnabled: readConfigBoolean(context, 'web.enabled', false),
282
299
  browserSurfacePublicBaseUrl: readConfigString(context, 'web.publicBaseUrl', '(not configured)'),
283
300
  activeRuntimeProfile: inferActiveRuntimeProfile(context.workspace?.shellPaths?.homeDirectory ?? ''),
@@ -1,5 +1,6 @@
1
1
  import type { AgentWorkspaceChannelStatus } from './agent-workspace-channels.ts';
2
2
  import type { AgentWorkspaceSetupChecklistItem } from './agent-workspace-setup.ts';
3
+ import type { AgentWorkspaceVoiceMediaReadiness } from './agent-workspace-voice-media.ts';
3
4
 
4
5
  export const AGENT_WORKSPACE_MODAL_NAME = 'agentWorkspace';
5
6
 
@@ -147,6 +148,7 @@ export interface AgentWorkspaceRuntimeSnapshot {
147
148
  readonly mediaProviderCount: number;
148
149
  readonly mediaUnderstandingProviderCount: number;
149
150
  readonly mediaGenerationProviderCount: number;
151
+ readonly voiceMediaReadiness: AgentWorkspaceVoiceMediaReadiness;
150
152
  readonly browserSurfaceEnabled: boolean;
151
153
  readonly browserSurfacePublicBaseUrl: string;
152
154
  readonly activeRuntimeProfile: string;
@@ -0,0 +1,216 @@
1
+ import type { CommandContext } from './command-registry.ts';
2
+
3
+ export type AgentWorkspaceVoiceMediaDomain = 'voice' | 'media';
4
+ export type AgentWorkspaceVoiceMediaSetupState = 'ready' | 'registered' | 'needs-secret' | 'not-registered';
5
+
6
+ export interface AgentWorkspaceVoiceMediaProviderDescriptor {
7
+ readonly id: string;
8
+ readonly label?: string;
9
+ readonly capabilities: readonly string[];
10
+ }
11
+
12
+ export interface AgentWorkspaceVoiceMediaProviderStatus {
13
+ readonly id: string;
14
+ readonly label: string;
15
+ readonly domain: AgentWorkspaceVoiceMediaDomain;
16
+ readonly features: readonly string[];
17
+ readonly setupState: AgentWorkspaceVoiceMediaSetupState;
18
+ readonly selected: boolean;
19
+ readonly secretKeyOptions: readonly string[];
20
+ readonly configuredSecretKeys: readonly string[];
21
+ readonly missingSecretKeyOptions: readonly string[];
22
+ readonly nextStep: string;
23
+ }
24
+
25
+ export interface AgentWorkspaceVoiceMediaReadiness {
26
+ readonly voiceProviders: readonly AgentWorkspaceVoiceMediaProviderStatus[];
27
+ readonly mediaProviders: readonly AgentWorkspaceVoiceMediaProviderStatus[];
28
+ readonly readyVoiceProviderCount: number;
29
+ readonly readyMediaProviderCount: number;
30
+ readonly selectedTtsProviderStatus: AgentWorkspaceVoiceMediaSetupState;
31
+ readonly selectedTtsProviderLabel: string;
32
+ readonly ttsVoiceConfigured: boolean;
33
+ readonly ttsResponseRouteConfigured: boolean;
34
+ readonly browserToolState: 'disabled' | 'local-only' | 'public-url';
35
+ readonly browserToolNextStep: string;
36
+ readonly nextSteps: readonly string[];
37
+ }
38
+
39
+ type AgentWorkspaceConfigReader = {
40
+ get(key: string): unknown;
41
+ };
42
+
43
+ type ProviderSecretSpec = {
44
+ readonly id: string;
45
+ readonly secretKeyOptions: readonly string[];
46
+ readonly noSecretRequired?: boolean;
47
+ };
48
+
49
+ const VOICE_SECRET_SPECS: readonly ProviderSecretSpec[] = [
50
+ { id: 'openai', secretKeyOptions: ['OPENAI_API_KEY', 'OPENAI_KEY'] },
51
+ { id: 'deepgram', secretKeyOptions: ['DEEPGRAM_API_KEY'] },
52
+ { id: 'google', secretKeyOptions: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_GEMINI_API_KEY'] },
53
+ { id: 'elevenlabs', secretKeyOptions: ['ELEVENLABS_API_KEY', 'XI_API_KEY'] },
54
+ { id: 'microsoft', secretKeyOptions: [], noSecretRequired: true },
55
+ { id: 'vydra', secretKeyOptions: ['VYDRA_API_KEY'] },
56
+ ];
57
+
58
+ const MEDIA_SECRET_SPECS: readonly ProviderSecretSpec[] = [
59
+ { id: 'builtin:image-understanding', secretKeyOptions: [], noSecretRequired: true },
60
+ { id: 'byteplus', secretKeyOptions: ['BYTEPLUS_API_KEY'] },
61
+ { id: 'runway', secretKeyOptions: ['RUNWAY_API_KEY', 'RUNWAYML_API_SECRET'] },
62
+ { id: 'alibaba', secretKeyOptions: ['MODELSTUDIO_API_KEY', 'DASHSCOPE_API_KEY', 'QWEN_API_KEY'] },
63
+ { id: 'fal', secretKeyOptions: ['FAL_KEY', 'FAL_API_KEY'] },
64
+ { id: 'comfy', secretKeyOptions: ['COMFY_BASE_URL', 'COMFY_API_KEY'] },
65
+ ];
66
+
67
+ function readConfigString(context: CommandContext, key: string, fallback: string): string {
68
+ try {
69
+ const configManager = context.platform?.configManager as unknown as AgentWorkspaceConfigReader | undefined;
70
+ const value = configManager?.get(key);
71
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : fallback;
72
+ } catch {
73
+ return fallback;
74
+ }
75
+ }
76
+
77
+ function readConfigBoolean(context: CommandContext, key: string, fallback: boolean): boolean {
78
+ try {
79
+ const configManager = context.platform?.configManager as unknown as AgentWorkspaceConfigReader | undefined;
80
+ const value = configManager?.get(key);
81
+ if (typeof value === 'boolean') return value;
82
+ if (typeof value === 'string') {
83
+ const normalized = value.trim().toLowerCase();
84
+ if (normalized === 'true') return true;
85
+ if (normalized === 'false') return false;
86
+ }
87
+ return fallback;
88
+ } catch {
89
+ return fallback;
90
+ }
91
+ }
92
+
93
+ function configuredSecretKeys(options: readonly string[]): readonly string[] {
94
+ return options.filter((key) => {
95
+ const value = process.env[key];
96
+ return typeof value === 'string' && value.trim().length > 0;
97
+ });
98
+ }
99
+
100
+ function specFor(domain: AgentWorkspaceVoiceMediaDomain, id: string): ProviderSecretSpec {
101
+ const specs = domain === 'voice' ? VOICE_SECRET_SPECS : MEDIA_SECRET_SPECS;
102
+ return specs.find((entry) => entry.id === id) ?? { id, secretKeyOptions: [] };
103
+ }
104
+
105
+ function providerLabel(provider: AgentWorkspaceVoiceMediaProviderDescriptor): string {
106
+ return typeof provider.label === 'string' && provider.label.trim().length > 0 ? provider.label.trim() : provider.id;
107
+ }
108
+
109
+ function buildProviderStatus(
110
+ domain: AgentWorkspaceVoiceMediaDomain,
111
+ provider: AgentWorkspaceVoiceMediaProviderDescriptor,
112
+ selectedProviderId: string,
113
+ ): AgentWorkspaceVoiceMediaProviderStatus {
114
+ const spec = specFor(domain, provider.id);
115
+ const configured = configuredSecretKeys(spec.secretKeyOptions);
116
+ const selected = selectedProviderId === provider.id;
117
+ const setupState: AgentWorkspaceVoiceMediaSetupState = spec.noSecretRequired || configured.length > 0
118
+ ? 'ready'
119
+ : spec.secretKeyOptions.length > 0
120
+ ? 'needs-secret'
121
+ : 'registered';
122
+ const label = providerLabel(provider);
123
+ const nextStep = setupState === 'ready'
124
+ ? `${label} is ready for explicit Agent voice/media use.`
125
+ : setupState === 'needs-secret'
126
+ ? `Configure one of ${spec.secretKeyOptions.join('|')} in the owning runtime environment.`
127
+ : `${label} is registered; confirm any provider-specific setup in the owning runtime.`;
128
+ return {
129
+ id: provider.id,
130
+ label,
131
+ domain,
132
+ features: provider.capabilities,
133
+ setupState,
134
+ selected,
135
+ secretKeyOptions: spec.secretKeyOptions,
136
+ configuredSecretKeys: configured,
137
+ missingSecretKeyOptions: configured.length > 0 || spec.noSecretRequired ? [] : spec.secretKeyOptions,
138
+ nextStep,
139
+ };
140
+ }
141
+
142
+ function missingSelectedProviderStatus(selectedProviderId: string): AgentWorkspaceVoiceMediaProviderStatus {
143
+ return {
144
+ id: selectedProviderId,
145
+ label: selectedProviderId,
146
+ domain: 'voice',
147
+ features: [],
148
+ setupState: 'not-registered',
149
+ selected: true,
150
+ secretKeyOptions: [],
151
+ configuredSecretKeys: [],
152
+ missingSecretKeyOptions: [],
153
+ nextStep: `Selected TTS provider ${selectedProviderId} is not registered in this runtime.`,
154
+ };
155
+ }
156
+
157
+ function selectedProviderStatus(
158
+ providers: readonly AgentWorkspaceVoiceMediaProviderStatus[],
159
+ selectedProviderId: string,
160
+ ): AgentWorkspaceVoiceMediaProviderStatus {
161
+ return providers.find((provider) => provider.id === selectedProviderId) ?? missingSelectedProviderStatus(selectedProviderId);
162
+ }
163
+
164
+ function browserToolState(context: CommandContext): AgentWorkspaceVoiceMediaReadiness['browserToolState'] {
165
+ if (!readConfigBoolean(context, 'web.enabled', false)) return 'disabled';
166
+ const publicBaseUrl = readConfigString(context, 'web.publicBaseUrl', '');
167
+ return publicBaseUrl ? 'public-url' : 'local-only';
168
+ }
169
+
170
+ function browserToolNextStep(state: AgentWorkspaceVoiceMediaReadiness['browserToolState']): string {
171
+ if (state === 'disabled') return 'Inspect MCP browser/automation tools; enable browser access in the owning runtime only when needed.';
172
+ if (state === 'local-only') return 'Browser tooling is local-only; keep external exposure off unless explicitly configured.';
173
+ return 'Public browser URL is configured; use explicit user action and runtime policy before browser-side effects.';
174
+ }
175
+
176
+ export function buildAgentWorkspaceVoiceMediaReadiness(options: {
177
+ readonly context: CommandContext;
178
+ readonly voiceProviders: readonly AgentWorkspaceVoiceMediaProviderDescriptor[];
179
+ readonly mediaProviders: readonly AgentWorkspaceVoiceMediaProviderDescriptor[];
180
+ }): AgentWorkspaceVoiceMediaReadiness {
181
+ const selectedTtsProviderId = readConfigString(options.context, 'tts.provider', '');
182
+ const ttsVoice = readConfigString(options.context, 'tts.voice', '');
183
+ const ttsLlmProvider = readConfigString(options.context, 'tts.llmProvider', '');
184
+ const ttsLlmModel = readConfigString(options.context, 'tts.llmModel', '');
185
+ const voiceProviders = options.voiceProviders.map((provider) => buildProviderStatus('voice', provider, selectedTtsProviderId));
186
+ const mediaProviders = options.mediaProviders.map((provider) => buildProviderStatus('media', provider, ''));
187
+ const selectedStatus = selectedTtsProviderId
188
+ ? selectedProviderStatus(voiceProviders, selectedTtsProviderId)
189
+ : {
190
+ ...missingSelectedProviderStatus('(provider default)'),
191
+ setupState: 'registered' as const,
192
+ nextStep: 'Choose a TTS provider when live speech should be predictable.',
193
+ };
194
+ const browserState = browserToolState(options.context);
195
+ const nextSteps = [
196
+ selectedStatus.setupState === 'needs-secret' ? selectedStatus.nextStep : '',
197
+ !ttsVoice ? 'Choose a stable TTS voice for day-one spoken replies.' : '',
198
+ mediaProviders.some((provider) => provider.features.includes('generate') && provider.setupState === 'needs-secret')
199
+ ? 'Configure at least one media generation provider secret before image/video generation.'
200
+ : '',
201
+ browserToolNextStep(browserState),
202
+ ].filter((step) => step.length > 0);
203
+ return {
204
+ voiceProviders,
205
+ mediaProviders,
206
+ readyVoiceProviderCount: voiceProviders.filter((provider) => provider.setupState === 'ready').length,
207
+ readyMediaProviderCount: mediaProviders.filter((provider) => provider.setupState === 'ready').length,
208
+ selectedTtsProviderStatus: selectedStatus.setupState,
209
+ selectedTtsProviderLabel: selectedStatus.label,
210
+ ttsVoiceConfigured: ttsVoice.length > 0,
211
+ ttsResponseRouteConfigured: ttsLlmProvider.length > 0 && ttsLlmModel.length > 0,
212
+ browserToolState: browserState,
213
+ browserToolNextStep: browserToolNextStep(browserState),
214
+ nextSteps,
215
+ };
216
+ }
@@ -205,23 +205,34 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
205
205
  const enabledCount = snapshot.channels.filter((channel) => channel.enabled).length;
206
206
  const readyCount = snapshot.channels.filter((channel) => channel.ready).length;
207
207
  const configuredDefaults = snapshot.channels.filter((channel) => channel.defaultTarget === 'configured').length;
208
- const disabledChannels = snapshot.channels.filter((channel) => !channel.enabled).map((channel) => channel.label).join(', ');
208
+ const readyChannels = snapshot.channels.filter((channel) => channel.ready).map((channel) => channel.label);
209
+ const needsTarget = snapshot.channels.filter((channel) => channel.setupState === 'needs-target');
210
+ const needsConfig = snapshot.channels.filter((channel) => channel.setupState === 'needs-config');
211
+ const disabledChannels = snapshot.channels.filter((channel) => !channel.enabled).map((channel) => channel.label);
212
+ const disabledPreview = disabledChannels.slice(0, 6).join(', ');
213
+ const disabledSuffix = disabledChannels.length > 6 ? `, +${disabledChannels.length - 6} more` : '';
214
+ const orderedChannels = [
215
+ ...snapshot.channels.filter((channel) => channel.enabled),
216
+ ...snapshot.channels.filter((channel) => !channel.enabled),
217
+ ].slice(0, 6);
209
218
  base.push(
210
219
  { text: `GoodVibes runtime: ${snapshot.daemonBaseUrl}`, fg: PALETTE.info },
211
220
  { text: `Readiness: ${readyCount}/${snapshot.channels.length} ready; ${enabledCount} enabled; ${configuredDefaults} default target(s) configured.`, fg: PALETTE.info },
212
- { text: `Disabled channels: ${disabledChannels || 'none'}.`, fg: PALETTE.dim },
213
- { text: 'Pairing: use /pair or /qrcode for companion setup.', fg: PALETTE.info },
214
- { text: 'Channel posture: inspect via /communication and /setup review.', fg: PALETTE.muted },
215
- { text: 'Safety: external delivery, unknown senders, and public exposure require explicit policy and user action.', fg: PALETTE.warn },
221
+ { text: `Ready channels: ${readyChannels.join(', ') || 'none'}.`, fg: readyChannels.length > 0 ? PALETTE.good : PALETTE.warn },
222
+ { text: `Needs default target: ${needsTarget.map((channel) => `${channel.label} -> ${channel.defaultTargetKeys.join('|')}`).join(', ') || 'none'}.`, fg: needsTarget.length > 0 ? PALETTE.warn : PALETTE.muted },
223
+ { text: `Needs config: ${needsConfig.map((channel) => `${channel.label} -> ${channel.missingRequiredKeys.join('|')}`).join(', ') || 'none'}.`, fg: needsConfig.length > 0 ? PALETTE.warn : PALETTE.muted },
224
+ { text: `Disabled channels: ${disabledPreview || 'none'}${disabledSuffix}.`, fg: PALETTE.dim },
225
+ { text: 'Safety: no secret values; sends and public exposure require explicit user action and runtime policy.', fg: PALETTE.warn },
216
226
  );
217
- for (const channel of snapshot.channels) {
218
- const enabled = channel.enabled ? 'enabled' : 'disabled';
227
+ for (const channel of orderedChannels) {
219
228
  const ready = channel.ready ? 'ready' : `${channel.missingConfigCount} missing`;
220
229
  base.push({
221
- text: `${channel.label}: ${enabled}; ${ready}; default ${channel.defaultTarget}; delivery ${channel.delivery}; risk ${channel.riskLabel}.`,
230
+ text: `${channel.label}: ${channel.setupState}; ${ready}; target ${channel.defaultTarget}; delivery ${channel.delivery}; risk ${channel.risk}.`,
222
231
  fg: channel.ready ? PALETTE.good : channel.enabled ? PALETTE.warn : PALETTE.dim,
223
232
  });
224
233
  }
234
+ base.push({ text: 'Only config key names and readiness state are rendered here.', fg: PALETTE.muted });
235
+ base.push({ text: 'Pairing: use /pair or /qrcode; inspect routed activity with /communication and /setup review.', fg: PALETTE.info });
225
236
  } else if (category.id === 'knowledge') {
226
237
  base.push(
227
238
  { text: `Route family: ${snapshot.knowledgeRoute}/{status,ask,search}`, fg: PALETTE.info },
@@ -231,13 +242,41 @@ function snapshotLines(workspace: AgentWorkspace, category: AgentWorkspaceCatego
231
242
  { text: 'Agent-owned content appears here only after explicit Agent knowledge ingestion.', fg: PALETTE.muted },
232
243
  );
233
244
  } else if (category.id === 'voice-media') {
245
+ const readiness = snapshot.voiceMediaReadiness;
246
+ const voiceRows = readiness.voiceProviders.slice(0, 6);
247
+ const mediaRows = readiness.mediaProviders.slice(0, 6);
234
248
  base.push(
235
249
  { text: `Voice providers: ${snapshot.voiceProviderCount}; streaming TTS: ${snapshot.voiceStreamingProviderCount}; STT: ${snapshot.voiceSttProviderCount}; realtime: ${snapshot.voiceRealtimeProviderCount}.`, fg: PALETTE.info },
236
- { text: `Voice interaction: ${snapshot.voiceSurfaceEnabled ? 'enabled' : 'disabled'}; use /voice review for portable voice posture.`, fg: snapshot.voiceSurfaceEnabled ? PALETTE.warn : PALETTE.muted },
250
+ { text: `Voice interaction: ${snapshot.voiceSurfaceEnabled ? 'enabled' : 'disabled'}; ready providers ${readiness.readyVoiceProviderCount}/${snapshot.voiceProviderCount}.`, fg: snapshot.voiceSurfaceEnabled ? PALETTE.warn : PALETTE.muted },
237
251
  { text: `TTS config: provider ${snapshot.ttsProvider}; voice ${snapshot.ttsVoice}; response model ${snapshot.ttsResponseModel}.`, fg: PALETTE.info },
252
+ { text: `Selected TTS readiness: ${readiness.selectedTtsProviderLabel} -> ${readiness.selectedTtsProviderStatus}; voice ${readiness.ttsVoiceConfigured ? 'configured' : 'default'}; response route ${readiness.ttsResponseRouteConfigured ? 'configured' : 'chat route'}.`, fg: readiness.selectedTtsProviderStatus === 'ready' ? PALETTE.good : PALETTE.warn },
238
253
  { text: `Media providers: ${snapshot.mediaProviderCount}; understanding: ${snapshot.mediaUnderstandingProviderCount}; generation: ${snapshot.mediaGenerationProviderCount}.`, fg: PALETTE.info },
239
- { text: `Browser companion: ${snapshot.browserSurfaceEnabled ? 'available' : 'not advertised'}; public base URL ${snapshot.browserSurfacePublicBaseUrl}.`, fg: snapshot.browserSurfaceEnabled ? PALETTE.warn : PALETTE.muted },
240
- { text: 'Build dispatch stays out of setup; explicit delegation is handled from the Build Delegation workspace.', fg: PALETTE.good },
254
+ { text: `Ready media providers: ${readiness.readyMediaProviderCount}/${snapshot.mediaProviderCount}.`, fg: readiness.readyMediaProviderCount > 0 ? PALETTE.good : PALETTE.warn },
255
+ { text: `Browser tooling: ${readiness.browserToolState}; public base URL ${snapshot.browserSurfacePublicBaseUrl}.`, fg: snapshot.browserSurfaceEnabled ? PALETTE.warn : PALETTE.muted },
256
+ { text: readiness.browserToolNextStep, fg: PALETTE.muted },
257
+ { text: 'Voice provider readiness', fg: PALETTE.title, bold: true },
258
+ );
259
+ for (const provider of voiceRows) {
260
+ const selected = provider.selected ? 'selected; ' : '';
261
+ const missing = provider.missingSecretKeyOptions.length > 0 ? `; needs ${provider.missingSecretKeyOptions.join('|')}` : '';
262
+ base.push({
263
+ text: `${provider.label}: ${selected}${provider.setupState}; ${provider.features.join(', ') || 'registered'}${missing}.`,
264
+ fg: provider.setupState === 'ready' ? PALETTE.good : provider.setupState === 'needs-secret' ? PALETTE.warn : PALETTE.muted,
265
+ });
266
+ }
267
+ if (snapshot.voiceProviderCount > voiceRows.length) base.push({ text: `${snapshot.voiceProviderCount - voiceRows.length} more voice provider(s).`, fg: PALETTE.dim });
268
+ base.push({ text: 'Media provider readiness', fg: PALETTE.title, bold: true });
269
+ for (const provider of mediaRows) {
270
+ const missing = provider.missingSecretKeyOptions.length > 0 ? `; needs ${provider.missingSecretKeyOptions.join('|')}` : '';
271
+ base.push({
272
+ text: `${provider.label}: ${provider.setupState}; ${provider.features.join(', ') || 'registered'}${missing}.`,
273
+ fg: provider.setupState === 'ready' ? PALETTE.good : provider.setupState === 'needs-secret' ? PALETTE.warn : PALETTE.muted,
274
+ });
275
+ }
276
+ if (snapshot.mediaProviderCount > mediaRows.length) base.push({ text: `${snapshot.mediaProviderCount - mediaRows.length} more media provider(s).`, fg: PALETTE.dim });
277
+ for (const step of readiness.nextSteps.slice(0, 4)) base.push({ text: `Next: ${step}`, fg: PALETTE.info });
278
+ base.push(
279
+ { text: 'No secret values are rendered. Voice, browser, and generated media side effects require explicit user action.', fg: PALETTE.warn },
241
280
  { text: 'Image input uses prompt attachments; media generation/provider setup stays behind explicit commands and configured providers.', fg: PALETTE.muted },
242
281
  );
243
282
  } else if (category.id === 'profiles') {
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.1.77';
9
+ let _version = '0.1.79';
10
10
  let _sdkVersion = '0.33.35';
11
11
  try {
12
12
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {