@pellux/goodvibes-agent 0.1.110 → 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.
- package/CHANGELOG.md +20 -0
- package/README.md +3 -3
- package/dist/package/main.js +3075 -2713
- package/docs/getting-started.md +3 -3
- package/package.json +1 -1
- package/src/cli/help.ts +0 -1
- package/src/cli/parser.ts +0 -8
- package/src/cli/types.ts +0 -1
- package/src/input/agent-workspace-categories.ts +5 -7
- package/src/input/agent-workspace-snapshot.ts +2 -10
- package/src/input/agent-workspace-types.ts +2 -3
- package/src/input/commands/brief-runtime.ts +1 -1
- package/src/input/commands/channels-runtime.ts +397 -35
- package/src/input/commands/experience-runtime.ts +4 -9
- package/src/input/commands/health-runtime.ts +26 -6
- package/src/input/commands/mcp-runtime.ts +51 -26
- package/src/input/commands/memory.ts +10 -10
- package/src/input/commands/planning-runtime.ts +4 -9
- package/src/input/commands/provider-accounts-runtime.ts +2 -3
- package/src/input/commands/qrcode-runtime.ts +48 -9
- package/src/input/commands/recall-bundle.ts +16 -16
- package/src/input/commands/recall-capture.ts +18 -18
- package/src/input/commands/recall-query.ts +22 -22
- package/src/input/commands/recall-review.ts +12 -12
- package/src/input/commands/tasks-runtime.ts +9 -15
- package/src/input/commands/work-plan-runtime.ts +5 -19
- package/src/input/commands.ts +2 -7
- package/src/renderer/agent-workspace.ts +1 -2
- package/src/runtime/onboarding/derivation.ts +6 -67
- package/src/version.ts +1 -1
package/docs/getting-started.md
CHANGED
|
@@ -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
|
|
100
|
-
/
|
|
101
|
-
/
|
|
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.
|
|
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
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
|
|
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,
|
|
120
|
-
detail: '
|
|
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: '
|
|
225
|
-
{ id: '
|
|
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
|
-
|
|
383
|
-
|
|
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
|
|
194
|
-
readonly
|
|
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
|
|
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(
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 {
|
|
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
|
-
|
|
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, /
|
|
62
|
+
' Related workspaces: /security, /trust, /mcp',
|
|
68
63
|
].join('\n'));
|
|
69
64
|
return;
|
|
70
65
|
}
|
|
71
|
-
ctx.print('Usage: /approval [
|
|
66
|
+
ctx.print('Usage: /approval [matrix|review <kind>]');
|
|
72
67
|
},
|
|
73
68
|
});
|
|
74
69
|
|