@pellux/goodvibes-agent 0.1.111 → 0.1.112

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.
@@ -96,9 +96,9 @@ Memory, personas, routines, and reusable Agent skills are local to GoodVibes Age
96
96
  /channels
97
97
  /agent-skills create --name "Morning Brief" --description "Daily briefing flow" --procedure "Check tasks, approvals, calendar, and unread state before summarizing." --enabled true
98
98
  /agent-skills enabled
99
- /skills local list
100
- /recall add fact Prefers concise morning briefings --scope project --tags preference
101
- /recall search morning
99
+ /skills list
100
+ /memory add fact "Prefers concise morning briefings" --scope project --tags preference
101
+ /memory search morning
102
102
  ```
103
103
 
104
104
  The active persona plus enabled Agent routines, reviewed memory, and skills are injected into the main serial assistant conversation. Starting a routine records local usage and prints its steps; it does not spawn background agents or automation jobs. Promoting a routine to a schedule is an explicit `schedules.create` call, requires `--yes`, writes a local redacted promotion receipt, and preserves the rule that Agent Knowledge never falls back to default Knowledge/Wiki or non-Agent knowledge segments.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.111",
3
+ "version": "0.1.112",
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",
package/src/cli/help.ts CHANGED
@@ -366,7 +366,6 @@ const HELP_ALIASES: Record<string, string> = {
366
366
  skill: 'skills',
367
367
  'agent-skills': 'skills',
368
368
  memories: 'memory',
369
- recall: 'memory',
370
369
  subscriptions: 'subscription',
371
370
  secret: 'secrets',
372
371
  session: 'sessions',
package/src/cli/parser.ts CHANGED
@@ -26,7 +26,6 @@ const COMMAND_ALIASES: Readonly<Record<string, GoodVibesCliCommand>> = {
26
26
  'agent-skills': 'skills',
27
27
  memory: 'memory',
28
28
  memories: 'memory',
29
- recall: 'memory',
30
29
  routines: 'routines',
31
30
  routine: 'routines',
32
31
  auth: 'auth',
@@ -66,7 +65,6 @@ function createDefaultFlags(): GoodVibesCliFlags {
66
65
  provider: undefined,
67
66
  model: undefined,
68
67
  agentProfile: undefined,
69
- daemonHome: undefined,
70
68
  runtimeUrl: undefined,
71
69
  workingDir: undefined,
72
70
  help: false,
@@ -286,12 +284,6 @@ export function parseGoodVibesCli(
286
284
  if (consumed.value !== undefined) flags = withFlag(flags, 'agentProfile', consumed.value);
287
285
  continue;
288
286
  }
289
- if (name === '--daemon-home') {
290
- const consumed = getValue(argv, index, inlineValue, name, errors);
291
- index = consumed.nextIndex;
292
- if (consumed.value !== undefined) flags = withFlag(flags, 'daemonHome', consumed.value);
293
- continue;
294
- }
295
287
  if (name === '--runtime-url' || name === '--runtime') {
296
288
  const consumed = getValue(argv, index, inlineValue, name, errors);
297
289
  index = consumed.nextIndex;
package/src/cli/types.ts CHANGED
@@ -39,7 +39,6 @@ export interface GoodVibesCliFlags {
39
39
  readonly provider: string | undefined;
40
40
  readonly model: string | undefined;
41
41
  readonly agentProfile: string | undefined;
42
- readonly daemonHome: string | undefined;
43
42
  readonly runtimeUrl: string | undefined;
44
43
  readonly workingDir: string | undefined;
45
44
  readonly help: boolean;
@@ -52,7 +52,7 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
52
52
  detail: 'Agent uses connected channel accounts. Pairing, account inspection, and readiness checks are visible here; inbound delivery and public channel exposure stay policy-gated.',
53
53
  actions: [
54
54
  { id: 'pair', label: 'Pair companion', detail: 'Open the QR pairing view for companion app setup.', command: '/pair', kind: 'command', safety: 'safe' },
55
- { id: 'channel-readiness', label: 'Channel readiness', detail: 'Show a read-only readiness matrix for configured messaging and notification channels.', command: '/channels', kind: 'command', safety: 'read-only' },
55
+ { id: 'channel-readiness', label: 'Channel readiness', detail: 'Show the read-only readiness matrix, then use /channels attention or /channels show <id> for setup detail.', command: '/channels', kind: 'command', safety: 'read-only' },
56
56
  { id: 'notification-routes', label: 'Notification routes', detail: 'Inspect configured webhook notification URLs without sending a test message.', command: '/notify list', kind: 'command', safety: 'read-only' },
57
57
  { id: 'notification-add-webhook', label: 'Add webhook target', detail: 'Open a confirmed form that adds one webhook notification target for explicit reminder and routine delivery.', editorKind: 'notify-webhook', kind: 'editor', safety: 'safe' },
58
58
  { id: 'notification-remove-webhook', label: 'Remove webhook target', detail: 'Open a confirmed form that removes one exact webhook notification target from Agent delivery.', editorKind: 'notify-webhook-remove', kind: 'editor', safety: 'safe' },
@@ -116,10 +116,9 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
116
116
  id: 'profiles',
117
117
  group: 'SETUP',
118
118
  label: 'Profiles',
119
- summary: 'Isolated Agent homes, config profiles, and setup bundles.',
120
- detail: 'Profiles isolate Agent state. Named homes and starter templates let one install behave like separate assistants for household, research, travel, operations, or personal workflows.',
119
+ summary: 'Isolated Agent homes, starter templates, and setup bundles.',
120
+ detail: 'Agent profiles isolate Agent state. Named homes and starter templates let one install behave like separate assistants for household, research, travel, operations, or personal workflows.',
121
121
  actions: [
122
- { id: 'profiles-open', label: 'Open config profiles', detail: 'Open the config profile picker for display/provider/behavior profile files.', command: '/profiles', kind: 'command', safety: 'safe' },
123
122
  { id: 'runtime-profile-guide', label: 'Starter authoring guide', detail: 'Open the Agent-local starter authoring flow inside the Agent TUI.', command: '/agent-profile guide', kind: 'command', safety: 'safe' },
124
123
  { id: 'runtime-profile-templates', label: 'Browse starter templates', detail: 'List built-in and local Agent starter templates with persona, skill, routine, and source details.', command: '/agent-profile templates', kind: 'command', safety: 'read-only' },
125
124
  { id: 'runtime-profile-list', label: 'List Agent profiles', detail: 'List isolated Agent profile homes under this Agent home.', command: '/agent-profile list', kind: 'command', safety: 'read-only' },
@@ -221,9 +220,8 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
221
220
  summary: 'Visible task state, work plan, and approval posture.',
222
221
  detail: 'Use this workspace to inspect active operator state. Side-effecting approval decisions require explicit commands and confirmation outside this workspace.',
223
222
  actions: [
224
- { id: 'workplan', label: 'Open work plan', detail: 'Open the workspace-scoped work plan panel.', command: '/workplan panel', kind: 'command', safety: 'read-only' },
225
- { id: 'workplan-list', label: 'List work plan', detail: 'Print a concise work plan summary.', command: '/workplan list', kind: 'command', safety: 'read-only' },
226
- { id: 'approvals', label: 'Review approvals', detail: 'Open/read approval posture. This workspace does not approve or deny requests.', command: '/approval open', kind: 'command', safety: 'read-only' },
223
+ { id: 'workplan', label: 'Review work plan', detail: 'Print a concise work plan summary in the main Agent transcript.', command: '/workplan list', kind: 'command', safety: 'read-only' },
224
+ { id: 'approvals', label: 'Review approvals', detail: 'Print the approval matrix without approving or denying requests.', command: '/approval matrix', kind: 'command', safety: 'read-only' },
227
225
  ],
228
226
  },
229
227
  {
@@ -240,13 +240,6 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
240
240
  return [];
241
241
  }
242
242
  })();
243
- const configProfileCount = (() => {
244
- try {
245
- return context.workspace?.profileManager?.list?.().length ?? 0;
246
- } catch {
247
- return 0;
248
- }
249
- })();
250
243
  const runtimeStarterTemplates = (() => {
251
244
  try {
252
245
  return listAgentRuntimeProfileTemplates(context.workspace?.shellPaths?.homeDirectory ?? '');
@@ -379,8 +372,8 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
379
372
  mcpConnectedServerCount: mcpSnapshot.connectedCount,
380
373
  mcpQuarantinedServerCount: mcpSnapshot.quarantinedCount,
381
374
  mcpAllowAllServerCount: mcpSnapshot.allowAllCount,
382
- browserSurfaceEnabled: readConfigBoolean(context, 'web.enabled', false),
383
- browserSurfacePublicBaseUrl: readConfigString(context, 'web.publicBaseUrl', '(not configured)'),
375
+ browserToolExposureEnabled: readConfigBoolean(context, 'web.enabled', false),
376
+ browserToolPublicBaseUrl: readConfigString(context, 'web.publicBaseUrl', '(not configured)'),
384
377
  activeRuntimeProfile: inferActiveRuntimeProfile(context.workspace?.shellPaths?.homeDirectory ?? ''),
385
378
  runtimeProfileCount: runtimeProfiles.length,
386
379
  runtimeProfiles: runtimeProfiles.map(summarizeRuntimeProfile),
@@ -388,7 +381,6 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
388
381
  runtimeStarterTemplateCount: runtimeStarterTemplates.length,
389
382
  localStarterTemplateCount: runtimeStarterTemplates.filter((template) => template.source === 'local').length,
390
383
  runtimeStarterTemplates: runtimeStarterTemplates.map(summarizeStarterTemplate),
391
- configProfileCount,
392
384
  setupChecklist,
393
385
  warnings,
394
386
  };
@@ -190,8 +190,8 @@ export interface AgentWorkspaceRuntimeSnapshot {
190
190
  readonly mcpConnectedServerCount: number;
191
191
  readonly mcpQuarantinedServerCount: number;
192
192
  readonly mcpAllowAllServerCount: number;
193
- readonly browserSurfaceEnabled: boolean;
194
- readonly browserSurfacePublicBaseUrl: string;
193
+ readonly browserToolExposureEnabled: boolean;
194
+ readonly browserToolPublicBaseUrl: string;
195
195
  readonly activeRuntimeProfile: string;
196
196
  readonly runtimeProfileCount: number;
197
197
  readonly runtimeProfiles: readonly AgentWorkspaceRuntimeProfileItem[];
@@ -199,7 +199,6 @@ export interface AgentWorkspaceRuntimeSnapshot {
199
199
  readonly runtimeStarterTemplateCount: number;
200
200
  readonly localStarterTemplateCount: number;
201
201
  readonly runtimeStarterTemplates: readonly AgentWorkspaceRuntimeStarterTemplateItem[];
202
- readonly configProfileCount: number;
203
202
  readonly setupChecklist: readonly AgentWorkspaceSetupChecklistItem[];
204
203
  readonly warnings: readonly string[];
205
204
  }
@@ -101,7 +101,7 @@ export function formatAgentOperatorBriefing(ctx: CommandContext): string {
101
101
  ` skills: ${snapshot.enabledSkillCount}/${snapshot.localSkillCount} enabled; bundles ${snapshot.enabledSkillBundleCount}/${snapshot.localSkillBundleCount}; active ${snapshot.activeSkillCount}`,
102
102
  ` routines: ${snapshot.enabledRoutineCount}/${snapshot.localRoutineCount} enabled`,
103
103
  ` channels: ${readyChannels}/${snapshot.channels.length} ready; ${enabledChannels} enabled`,
104
- ` voice/media: ${snapshot.voiceProviderCount} voice, ${snapshot.mediaProviderCount} media; browser surface ${snapshot.browserSurfaceEnabled ? 'enabled' : 'disabled'}`,
104
+ ` voice/media: ${snapshot.voiceProviderCount} voice, ${snapshot.mediaProviderCount} media; browser tools ${snapshot.voiceMediaReadiness.browserToolState}`,
105
105
  formatWorkPlanLine(workPlan.total, workPlan.counts),
106
106
  ` schedules: ${enabledJobs}/${jobs.length} visible jobs enabled`,
107
107
  '',
@@ -1,47 +1,409 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
1
3
  import type { CommandRegistry } from '../command-registry.ts';
4
+ import type { CommandContext } from '../command-registry.ts';
5
+ import type { AgentWorkspaceChannelStatus } from '../agent-workspace-channels.ts';
2
6
  import { buildAgentWorkspaceChannels } from '../agent-workspace-channels.ts';
3
7
 
8
+ type ChannelFilter = 'all' | 'ready' | 'attention';
9
+ type JsonRecord = Record<string, unknown>;
10
+
11
+ interface ChannelDaemonConnection {
12
+ readonly baseUrl: string;
13
+ readonly token: string | null;
14
+ readonly tokenPath: string;
15
+ }
16
+
17
+ interface ChannelRouteSuccess {
18
+ readonly ok: true;
19
+ readonly route: string;
20
+ readonly body: unknown;
21
+ }
22
+
23
+ interface ChannelRouteFailure {
24
+ readonly ok: false;
25
+ readonly route: string;
26
+ readonly kind: 'auth_required' | 'daemon_unavailable' | 'route_unavailable' | 'daemon_error';
27
+ readonly message: string;
28
+ }
29
+
30
+ type ChannelRouteResult = ChannelRouteSuccess | ChannelRouteFailure;
31
+
32
+ function isRecord(value: unknown): value is JsonRecord {
33
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
34
+ }
35
+
36
+ function readString(record: JsonRecord, key: string, fallback = ''): string {
37
+ const value = record[key];
38
+ return typeof value === 'string' ? value : fallback;
39
+ }
40
+
41
+ function readBoolean(record: JsonRecord, key: string, fallback = false): boolean {
42
+ const value = record[key];
43
+ return typeof value === 'boolean' ? value : fallback;
44
+ }
45
+
46
+ function readRecordArray(record: JsonRecord, key: string): readonly JsonRecord[] {
47
+ const value = record[key];
48
+ return Array.isArray(value) ? value.filter(isRecord) : [];
49
+ }
50
+
51
+ function resolveChannelDaemonConnection(context: CommandContext): ChannelDaemonConnection {
52
+ const hostValue = context.platform?.configManager?.get('controlPlane.host');
53
+ const portValue = context.platform?.configManager?.get('controlPlane.port');
54
+ const host = typeof hostValue === 'string' && hostValue.trim().length > 0 ? hostValue.trim() : '127.0.0.1';
55
+ const port = typeof portValue === 'number' && Number.isFinite(portValue) ? portValue : 3421;
56
+ const homeDirectory = context.workspace?.shellPaths?.homeDirectory ?? process.env.HOME ?? '';
57
+ const tokenPath = join(homeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
58
+ if (!existsSync(tokenPath)) return { baseUrl: `http://${host}:${port}`, token: null, tokenPath };
59
+ try {
60
+ const parsed = JSON.parse(readFileSync(tokenPath, 'utf-8')) as unknown;
61
+ const token = isRecord(parsed) && typeof parsed.token === 'string' ? parsed.token : null;
62
+ return { baseUrl: `http://${host}:${port}`, token, tokenPath };
63
+ } catch {
64
+ return { baseUrl: `http://${host}:${port}`, token: null, tokenPath };
65
+ }
66
+ }
67
+
68
+ async function fetchChannelRoute(context: CommandContext, route: string): Promise<ChannelRouteResult> {
69
+ const connection = resolveChannelDaemonConnection(context);
70
+ if (!connection.token) {
71
+ return {
72
+ ok: false,
73
+ route,
74
+ kind: 'auth_required',
75
+ message: `No runtime operator token found at ${connection.tokenPath}`,
76
+ };
77
+ }
78
+
79
+ try {
80
+ const response = await fetch(`${connection.baseUrl}${route}`, {
81
+ headers: { authorization: `Bearer ${connection.token}` },
82
+ });
83
+ const text = await response.text();
84
+ let body: unknown = text;
85
+ if (text.trim().length > 0) {
86
+ try {
87
+ body = JSON.parse(text) as unknown;
88
+ } catch {
89
+ body = text;
90
+ }
91
+ }
92
+ if (!response.ok) {
93
+ const detail = isRecord(body) && typeof body.error === 'string' ? body.error : text;
94
+ return {
95
+ ok: false,
96
+ route,
97
+ kind: response.status === 401 || response.status === 403
98
+ ? 'auth_required'
99
+ : response.status === 404
100
+ ? 'route_unavailable'
101
+ : 'daemon_error',
102
+ message: `HTTP ${response.status}${detail ? `: ${detail}` : ''}`,
103
+ };
104
+ }
105
+ return { ok: true, route, body };
106
+ } catch (error) {
107
+ return {
108
+ ok: false,
109
+ route,
110
+ kind: 'daemon_unavailable',
111
+ message: error instanceof Error ? error.message : String(error),
112
+ };
113
+ }
114
+ }
115
+
116
+ function formatChannelRouteFailure(title: string, failure: ChannelRouteFailure): string {
117
+ return [
118
+ `${title}: unavailable`,
119
+ ` kind: ${failure.kind}`,
120
+ ` route: ${failure.route}`,
121
+ ` error: ${failure.message}`,
122
+ ' policy: read-only; no channel send/action route was called',
123
+ ].join('\n');
124
+ }
125
+
126
+ function formatChannelLine(channel: AgentWorkspaceChannelStatus): string {
127
+ const missing = channel.missingRequiredKeys.length > 0
128
+ ? ` missing=${channel.missingRequiredKeys.join('|')}`
129
+ : '';
130
+ const target = channel.configuredDefaultTargetKeys.length > 0
131
+ ? ` target=${channel.configuredDefaultTargetKeys.join('|')}`
132
+ : channel.defaultTargetKeys.length > 0
133
+ ? ` target=missing(${channel.defaultTargetKeys.join('|')})`
134
+ : ' target=not-required';
135
+ return [
136
+ ` ${channel.label}: ${channel.setupState}`,
137
+ `ready=${channel.ready ? 'yes' : 'no'}`,
138
+ `delivery=${channel.delivery}`,
139
+ `risk=${channel.risk}`,
140
+ target,
141
+ missing,
142
+ ].join(' ');
143
+ }
144
+
145
+ function filterChannels(
146
+ channels: readonly AgentWorkspaceChannelStatus[],
147
+ filter: ChannelFilter,
148
+ ): readonly AgentWorkspaceChannelStatus[] {
149
+ if (filter === 'ready') return channels.filter((channel) => channel.ready);
150
+ if (filter === 'attention') return channels.filter((channel) => channel.enabled && channel.setupState !== 'ready');
151
+ return channels;
152
+ }
153
+
154
+ function printChannelSummary(
155
+ print: (message: string) => void,
156
+ channels: readonly AgentWorkspaceChannelStatus[],
157
+ filter: ChannelFilter,
158
+ ): void {
159
+ const ready = channels.filter((channel) => channel.ready);
160
+ const enabled = channels.filter((channel) => channel.enabled);
161
+ const needsTarget = channels.filter((channel) => channel.setupState === 'needs-target');
162
+ const needsConfig = channels.filter((channel) => channel.setupState === 'needs-config');
163
+ const filtered = filterChannels(channels, filter);
164
+ const title = filter === 'ready'
165
+ ? 'Channel Readiness: Ready'
166
+ : filter === 'attention'
167
+ ? 'Channel Readiness: Needs Attention'
168
+ : 'Channel Readiness';
169
+ const lines: string[] = [
170
+ title,
171
+ ` ready: ${ready.length}/${channels.length}`,
172
+ ` enabled: ${enabled.length}/${channels.length}`,
173
+ ` needs target: ${needsTarget.length}`,
174
+ ` needs config: ${needsConfig.length}`,
175
+ ' policy: read-only inspection; sends require explicit user action and Agent policy',
176
+ ' details: /channels show <id>',
177
+ '',
178
+ ...(filtered.length > 0
179
+ ? filtered.map(formatChannelLine)
180
+ : [` No ${filter === 'ready' ? 'ready' : 'attention'} channels matched.`]),
181
+ ];
182
+ print(lines.join('\n'));
183
+ }
184
+
185
+ function printChannelDetail(
186
+ print: (message: string) => void,
187
+ channel: AgentWorkspaceChannelStatus,
188
+ ): void {
189
+ print([
190
+ `Channel: ${channel.label} (${channel.id})`,
191
+ ` state: ${channel.setupState}`,
192
+ ` enabled: ${channel.enabled ? 'yes' : 'no'}`,
193
+ ` ready: ${channel.ready ? 'yes' : 'no'}`,
194
+ ` delivery: ${channel.delivery}`,
195
+ ` risk: ${channel.risk} (${channel.riskLabel})`,
196
+ ` required config keys: ${channel.requiredKeys.join(', ') || 'none'}`,
197
+ ` missing config keys: ${channel.missingRequiredKeys.join(', ') || 'none'}`,
198
+ ` default target keys: ${channel.defaultTargetKeys.join(', ') || 'not required'}`,
199
+ ` configured target keys: ${channel.configuredDefaultTargetKeys.join(', ') || 'none'}`,
200
+ ` next: ${channel.nextStep}`,
201
+ ' policy: this command never prints secret values and never sends messages',
202
+ ].join('\n'));
203
+ }
204
+
205
+ function formatChannelAccounts(body: unknown): string {
206
+ const root = isRecord(body) ? body : {};
207
+ const accounts = readRecordArray(root, 'accounts');
208
+ const lines = [
209
+ 'Channel Accounts',
210
+ ` accounts: ${accounts.length}`,
211
+ ' policy: read-only account posture; secret values are never shown',
212
+ '',
213
+ ];
214
+ if (accounts.length === 0) return [...lines, ' No channel accounts reported by connected services.'].join('\n');
215
+ for (const account of accounts.slice(0, 20)) {
216
+ const surface = readString(account, 'surface', 'unknown');
217
+ const accountId = readString(account, 'accountId', '');
218
+ const authState = readString(account, 'authState', readString(account, 'state', 'unknown'));
219
+ const configured = readBoolean(account, 'configured') ? 'configured' : 'not-configured';
220
+ const linked = readBoolean(account, 'linked') ? 'linked' : 'not-linked';
221
+ const secrets = readRecordArray(account, 'secrets')
222
+ .map((secret) => `${readString(secret, 'field', 'secret')}:${readString(secret, 'source', 'configured')}`)
223
+ .join(', ') || 'none';
224
+ lines.push(` ${surface}${accountId ? `/${accountId}` : ''}: ${configured}; ${linked}; auth=${authState}; secret refs=${secrets}`);
225
+ }
226
+ if (accounts.length > 20) lines.push(` ${accounts.length - 20} more account(s) omitted.`);
227
+ return lines.join('\n');
228
+ }
229
+
230
+ function formatChannelPolicies(body: unknown): string {
231
+ const root = isRecord(body) ? body : {};
232
+ const policies = readRecordArray(root, 'policies');
233
+ const lines = [
234
+ 'Channel Policies',
235
+ ` policies: ${policies.length}`,
236
+ ' policy: read-only policy posture; use exact confirmed commands for changes',
237
+ '',
238
+ ];
239
+ if (policies.length === 0) return [...lines, ' No channel policies reported by connected services.'].join('\n');
240
+ for (const policy of policies.slice(0, 20)) {
241
+ const surface = readString(policy, 'surface', 'unknown');
242
+ const direct = readBoolean(policy, 'allowDirectMessages') ? 'direct=yes' : 'direct=no';
243
+ const users = Array.isArray(policy.allowlistUserIds) ? policy.allowlistUserIds.length : 0;
244
+ const groups = Array.isArray(policy.allowlistGroupIds) ? policy.allowlistGroupIds.length : 0;
245
+ const groupPolicies = readRecordArray(policy, 'groupPolicies').length;
246
+ lines.push(` ${surface}: ${direct}; allowlist users=${users}; groups=${groups}; group policies=${groupPolicies}`);
247
+ }
248
+ if (policies.length > 20) lines.push(` ${policies.length - 20} more policy record(s) omitted.`);
249
+ return lines.join('\n');
250
+ }
251
+
252
+ function formatChannelStatus(body: unknown): string {
253
+ const root = isRecord(body) ? body : {};
254
+ const channels = readRecordArray(root, 'channels');
255
+ const lines = [
256
+ 'Connected Channel Status',
257
+ ` channels: ${channels.length}`,
258
+ ' policy: read-only connected-service status',
259
+ '',
260
+ ];
261
+ if (channels.length === 0) return [...lines, ' No connected channel status reported.'].join('\n');
262
+ for (const channel of channels.slice(0, 20)) {
263
+ const surface = readString(channel, 'surface', 'unknown');
264
+ const state = readString(channel, 'state', readString(channel, 'status', 'unknown'));
265
+ const enabled = readBoolean(channel, 'enabled') ? 'enabled' : 'disabled';
266
+ const ready = readBoolean(channel, 'ready') ? 'ready' : 'not-ready';
267
+ lines.push(` ${surface}: ${enabled}; ${ready}; state=${state}`);
268
+ }
269
+ if (channels.length > 20) lines.push(` ${channels.length - 20} more channel(s) omitted.`);
270
+ return lines.join('\n');
271
+ }
272
+
273
+ function formatChannelDoctor(surface: string, body: unknown): string {
274
+ const root = isRecord(body) ? body : {};
275
+ const checks = readRecordArray(root, 'checks');
276
+ const repairActions = readRecordArray(root, 'repairActions');
277
+ const lines = [
278
+ `Channel Doctor: ${readString(root, 'surface', surface)}`,
279
+ ` checks: ${checks.length}`,
280
+ ` repair actions: ${repairActions.length}`,
281
+ ' policy: read-only doctor report; repair actions are not run here',
282
+ '',
283
+ ];
284
+ if (checks.length === 0) lines.push(' No doctor checks reported.');
285
+ for (const check of checks.slice(0, 20)) {
286
+ lines.push(` ${readString(check, 'id', 'check')}: ${readString(check, 'status', 'unknown')}`);
287
+ }
288
+ if (repairActions.length > 0) {
289
+ lines.push('', ' Available repair action ids:');
290
+ for (const action of repairActions.slice(0, 12)) lines.push(` - ${readString(action, 'id', 'action')}`);
291
+ }
292
+ return lines.join('\n');
293
+ }
294
+
295
+ function formatChannelSetup(surface: string, body: unknown): string {
296
+ const root = isRecord(body) ? body : {};
297
+ const fields = readRecordArray(root, 'fields');
298
+ const secretTargets = readRecordArray(root, 'secretTargets');
299
+ const lines = [
300
+ `Channel Setup Schema: ${readString(root, 'surface', surface)}`,
301
+ ` version: ${typeof root.version === 'number' ? root.version : 'unknown'}`,
302
+ ` fields: ${fields.length}`,
303
+ ` secret targets: ${secretTargets.length}`,
304
+ ' policy: read-only setup schema; no credentials or values are printed',
305
+ '',
306
+ ];
307
+ if (fields.length > 0) {
308
+ lines.push(' Fields:');
309
+ for (const field of fields.slice(0, 20)) lines.push(` - ${readString(field, 'id', 'field')}`);
310
+ }
311
+ if (secretTargets.length > 0) {
312
+ lines.push('', ' Secret targets:');
313
+ for (const target of secretTargets.slice(0, 12)) {
314
+ const required = readBoolean(target, 'required') ? 'required' : 'optional';
315
+ lines.push(` - ${readString(target, 'id', 'secret')} (${required})`);
316
+ }
317
+ }
318
+ return lines.join('\n');
319
+ }
320
+
321
+ async function printReadOnlyChannelRoute(
322
+ context: CommandContext,
323
+ title: string,
324
+ route: string,
325
+ format: (body: unknown) => string,
326
+ ): Promise<void> {
327
+ const result = await fetchChannelRoute(context, route);
328
+ if (!result.ok) {
329
+ context.print(formatChannelRouteFailure(title, result));
330
+ return;
331
+ }
332
+ context.print(format(result.body));
333
+ }
334
+
4
335
  export function registerChannelsRuntimeCommands(registry: CommandRegistry): void {
5
336
  registry.register({
6
337
  name: 'channels',
7
338
  aliases: ['channel'],
8
339
  description: 'Inspect Agent channel readiness without sending messages',
9
- usage: '[list|readiness]',
10
- argsHint: 'list|readiness',
11
- handler(_args, ctx) {
340
+ usage: '[list|readiness|ready|attention|show <id>|accounts|policies|status|doctor <id>|setup <id>]',
341
+ argsHint: 'list|readiness|ready|attention|show|accounts|policies|status|doctor|setup',
342
+ async handler(args, ctx) {
12
343
  const channels = buildAgentWorkspaceChannels(ctx);
13
- const ready = channels.filter((channel) => channel.ready);
14
- const enabled = channels.filter((channel) => channel.enabled);
15
- const needsTarget = channels.filter((channel) => channel.setupState === 'needs-target');
16
- const needsConfig = channels.filter((channel) => channel.setupState === 'needs-config');
17
- const lines: string[] = [
18
- 'Channel Readiness',
19
- ` ready: ${ready.length}/${channels.length}`,
20
- ` enabled: ${enabled.length}/${channels.length}`,
21
- ` needs target: ${needsTarget.length}`,
22
- ` needs config: ${needsConfig.length}`,
23
- ' policy: read-only inspection; sends require explicit user action and Agent policy',
24
- '',
25
- ...channels.map((channel) => {
26
- const missing = channel.missingRequiredKeys.length > 0
27
- ? ` missing=${channel.missingRequiredKeys.join('|')}`
28
- : '';
29
- const target = channel.configuredDefaultTargetKeys.length > 0
30
- ? ` target=${channel.configuredDefaultTargetKeys.join('|')}`
31
- : channel.defaultTargetKeys.length > 0
32
- ? ` target=missing(${channel.defaultTargetKeys.join('|')})`
33
- : ' target=not-required';
34
- return [
35
- ` ${channel.label}: ${channel.setupState}`,
36
- `ready=${channel.ready ? 'yes' : 'no'}`,
37
- `delivery=${channel.delivery}`,
38
- `risk=${channel.risk}`,
39
- target,
40
- missing,
41
- ].join(' ');
42
- }),
43
- ];
44
- ctx.print(lines.join('\n'));
344
+ const subcommand = (args[0] ?? 'readiness').trim().toLowerCase();
345
+
346
+ if (subcommand === 'list' || subcommand === 'readiness') {
347
+ printChannelSummary(ctx.print, channels, 'all');
348
+ return;
349
+ }
350
+
351
+ if (subcommand === 'ready') {
352
+ printChannelSummary(ctx.print, channels, 'ready');
353
+ return;
354
+ }
355
+
356
+ if (subcommand === 'attention' || subcommand === 'issues') {
357
+ printChannelSummary(ctx.print, channels, 'attention');
358
+ return;
359
+ }
360
+
361
+ if (subcommand === 'show') {
362
+ const channelId = args[1]?.trim().toLowerCase();
363
+ if (!channelId) {
364
+ ctx.print('Usage: /channels show <id>');
365
+ return;
366
+ }
367
+ const channel = channels.find((entry) => entry.id.toLowerCase() === channelId || entry.label.toLowerCase() === channelId);
368
+ if (!channel) {
369
+ ctx.print(`Unknown channel: ${channelId}\nUse /channels list to see available channel ids.`);
370
+ return;
371
+ }
372
+ printChannelDetail(ctx.print, channel);
373
+ return;
374
+ }
375
+
376
+ if (subcommand === 'accounts') {
377
+ await printReadOnlyChannelRoute(ctx, 'Channel accounts', '/api/channels/accounts', formatChannelAccounts);
378
+ return;
379
+ }
380
+
381
+ if (subcommand === 'policies') {
382
+ await printReadOnlyChannelRoute(ctx, 'Channel policies', '/api/channels/policies', formatChannelPolicies);
383
+ return;
384
+ }
385
+
386
+ if (subcommand === 'status') {
387
+ await printReadOnlyChannelRoute(ctx, 'Channel status', '/api/channels/status', formatChannelStatus);
388
+ return;
389
+ }
390
+
391
+ if (subcommand === 'doctor' || subcommand === 'setup') {
392
+ const channelId = args[1]?.trim().toLowerCase();
393
+ if (!channelId) {
394
+ ctx.print(`Usage: /channels ${subcommand} <id>`);
395
+ return;
396
+ }
397
+ const encoded = encodeURIComponent(channelId);
398
+ if (subcommand === 'doctor') {
399
+ await printReadOnlyChannelRoute(ctx, 'Channel doctor', `/api/channels/doctor/${encoded}`, (body) => formatChannelDoctor(channelId, body));
400
+ return;
401
+ }
402
+ await printReadOnlyChannelRoute(ctx, 'Channel setup', `/api/channels/setup/${encoded}`, (body) => formatChannelSetup(channelId, body));
403
+ return;
404
+ }
405
+
406
+ ctx.print('Usage: /channels [list|readiness|ready|attention|show <id>|accounts|policies|status|doctor <id>|setup <id>]');
45
407
  },
46
408
  });
47
409
  }
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, resolve } from 'node:path';
3
3
  import type { CommandRegistry } from '../command-registry.ts';
4
- import { requirePanelManager, requireShellPaths } from './runtime-services.ts';
4
+ import { requireShellPaths } from './runtime-services.ts';
5
5
  import { requireYesFlag, stripYesFlag } from './confirmation.ts';
6
6
 
7
7
  interface VoiceBundle {
@@ -29,12 +29,7 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
29
29
  async handler(args, ctx) {
30
30
  const sub = (args[0] ?? 'matrix').toLowerCase();
31
31
  if (sub === 'open' || sub === 'panel') {
32
- if (ctx.showPanel) ctx.showPanel('approval');
33
- else {
34
- const panelManager = requirePanelManager(ctx);
35
- panelManager.open('approval');
36
- panelManager.show();
37
- }
32
+ ctx.print('Approval panels are not part of the Agent workspace. Use /approval matrix.');
38
33
  return;
39
34
  }
40
35
  const matrix = [
@@ -64,11 +59,11 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
64
59
  ctx.print([
65
60
  `Approval Review: ${entry[0]}`,
66
61
  ` ${entry[1]}`,
67
- ' Related workspaces: /security, /policy preflight, /trust, /mcp',
62
+ ' Related workspaces: /security, /trust, /mcp',
68
63
  ].join('\n'));
69
64
  return;
70
65
  }
71
- ctx.print('Usage: /approval [open|matrix|review <kind>]');
66
+ ctx.print('Usage: /approval [matrix|review <kind>]');
72
67
  },
73
68
  });
74
69