@pellux/goodvibes-agent 0.1.1 → 0.1.2
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 +8 -0
- package/docs/getting-started.md +1 -1
- package/docs/release-and-publishing.md +1 -1
- package/package.json +1 -1
- package/src/cli/agent-knowledge-command.ts +525 -0
- package/src/cli/help.ts +35 -0
- package/src/cli/management.ts +33 -9
- package/src/cli/parser.ts +7 -0
- package/src/cli/types.ts +3 -0
- package/src/input/agent-workspace.ts +1 -1
- package/src/input/commands/delegation-runtime.ts +129 -0
- package/src/input/commands/shell-core.ts +9 -6
- package/src/input/commands.ts +2 -0
- package/src/input/submission-router.ts +1 -1
- package/src/panels/builtin/agent.ts +0 -14
- package/src/runtime/bootstrap-shell.ts +3 -2
- package/src/version.ts +1 -1
- package/LICENSE +0 -21
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GoodVibes Agent will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.1.2 - 2026-05-30
|
|
6
|
+
|
|
7
|
+
- Added `goodvibes-agent compat` for package SDK pin, external daemon version, auth presence, and isolated Agent Knowledge route readiness.
|
|
8
|
+
- Added `goodvibes-agent knowledge ...` commands for the isolated `/api/goodvibes-agent/knowledge/*` environment with no default Knowledge/Wiki or HomeGraph fallback.
|
|
9
|
+
- Added explicit GoodVibes TUI build delegation through `goodvibes-agent delegate` and `/delegate`; WRFC is requested only through explicit `--wrfc`, `/wrfc`, or `/review` delegation.
|
|
10
|
+
- Removed the copied WRFC panel from the default Agent panel registry while preserving explicit TUI delegation for build/fix/review work.
|
|
11
|
+
- Hardened the Agent release helper and CLI help output for the current Agent changelog and command set.
|
|
12
|
+
|
|
5
13
|
## 0.1.1 - 2026-05-30
|
|
6
14
|
|
|
7
15
|
- Reissued the first public alpha package after the initial `0.1.0` registry publish produced an install-blocking npm packument inconsistency.
|
package/docs/getting-started.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Getting Started
|
|
2
2
|
|
|
3
|
-
GoodVibes Agent `0.1.
|
|
3
|
+
GoodVibes Agent `0.1.2` is the current installable public alpha of the personal operator assistant built on the GoodVibes TUI foundation.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Near-fork GoodVibes operator assistant with the GoodVibes TUI shell, renderer, input, fullscreen workspace, and daemon-connected Agent product brain.",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { createBrowserAgentSdk } from '@pellux/goodvibes-sdk/browser/agent';
|
|
5
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
|
+
import type { CliCommandOutput } from './types.ts';
|
|
7
|
+
import type { CliCommandRuntime } from './management.ts';
|
|
8
|
+
import { formatJsonOrText, yesNo } from './management.ts';
|
|
9
|
+
|
|
10
|
+
type JsonRecord = Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
interface AgentDaemonConnection {
|
|
13
|
+
readonly baseUrl: string;
|
|
14
|
+
readonly token: string | null;
|
|
15
|
+
readonly tokenPath: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface AgentKnowledgeFailure {
|
|
19
|
+
readonly ok: false;
|
|
20
|
+
readonly kind: 'daemon_unavailable' | 'auth_required' | 'daemon_route_unavailable' | 'daemon_error';
|
|
21
|
+
readonly error: string;
|
|
22
|
+
readonly baseUrl: string;
|
|
23
|
+
readonly route: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface AgentKnowledgeSuccess<TData> {
|
|
27
|
+
readonly ok: true;
|
|
28
|
+
readonly kind: string;
|
|
29
|
+
readonly route: string;
|
|
30
|
+
readonly data: TData;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type AgentKnowledgeResult<TData> = AgentKnowledgeSuccess<TData> | AgentKnowledgeFailure;
|
|
34
|
+
|
|
35
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
36
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readString(record: JsonRecord | null, key: string): string | null {
|
|
40
|
+
const value = record?.[key];
|
|
41
|
+
return typeof value === 'string' ? value : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readNumber(record: JsonRecord | null, key: string): number | null {
|
|
45
|
+
const value = record?.[key];
|
|
46
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readBoolean(record: JsonRecord | null, key: string): boolean | null {
|
|
50
|
+
const value = record?.[key];
|
|
51
|
+
return typeof value === 'boolean' ? value : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readArray(record: JsonRecord | null, key: string): readonly unknown[] {
|
|
55
|
+
const value = record?.[key];
|
|
56
|
+
return Array.isArray(value) ? value : [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cleanInline(value: unknown): string {
|
|
60
|
+
return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function commandValues(args: readonly string[]): string[] {
|
|
64
|
+
const values: string[] = [];
|
|
65
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
66
|
+
const token = args[index]!;
|
|
67
|
+
if (!token.startsWith('--')) {
|
|
68
|
+
values.push(token);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!token.includes('=') && args[index + 1] && !args[index + 1]!.startsWith('--')) index += 1;
|
|
72
|
+
}
|
|
73
|
+
return values;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function delegationTaskValues(args: readonly string[]): string[] {
|
|
77
|
+
const values: string[] = [];
|
|
78
|
+
for (const token of args) {
|
|
79
|
+
if (token === '--wrfc') continue;
|
|
80
|
+
if (!token.startsWith('--')) values.push(token);
|
|
81
|
+
}
|
|
82
|
+
return values;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readOptionValue(args: readonly string[], name: string): string | undefined {
|
|
86
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
87
|
+
const token = args[index]!;
|
|
88
|
+
if (token === name) {
|
|
89
|
+
const next = args[index + 1];
|
|
90
|
+
return next && !next.startsWith('--') ? next : undefined;
|
|
91
|
+
}
|
|
92
|
+
if (token.startsWith(`${name}=`)) return token.slice(name.length + 1);
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readPositiveInt(args: readonly string[], name: string, fallback: number): number {
|
|
98
|
+
const raw = readOptionValue(args, name);
|
|
99
|
+
if (!raw) return fallback;
|
|
100
|
+
const parsed = Number.parseInt(raw, 10);
|
|
101
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readStringList(args: readonly string[], name: string): readonly string[] {
|
|
105
|
+
const raw = readOptionValue(args, name);
|
|
106
|
+
if (!raw) return [];
|
|
107
|
+
return raw.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function hasFlag(args: readonly string[], flag: string): boolean {
|
|
111
|
+
return args.includes(flag);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function packageJsonPath(): string {
|
|
115
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readPackageMetadata(): { readonly version: string; readonly sdkVersion: string } {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath(), 'utf-8')) as unknown;
|
|
121
|
+
if (!isRecord(parsed)) return { version: 'unknown', sdkVersion: 'unknown' };
|
|
122
|
+
const dependencies = isRecord(parsed.dependencies) ? parsed.dependencies : {};
|
|
123
|
+
return {
|
|
124
|
+
version: typeof parsed.version === 'string' ? parsed.version : 'unknown',
|
|
125
|
+
sdkVersion: typeof dependencies['@pellux/goodvibes-sdk'] === 'string'
|
|
126
|
+
? dependencies['@pellux/goodvibes-sdk']
|
|
127
|
+
: 'unknown',
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return { version: 'unknown', sdkVersion: 'unknown' };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveDaemonConnection(runtime: CliCommandRuntime): AgentDaemonConnection {
|
|
135
|
+
const host = String(runtime.configManager.get('controlPlane.host') ?? '127.0.0.1');
|
|
136
|
+
const port = Number(runtime.configManager.get('controlPlane.port') ?? 3421);
|
|
137
|
+
const baseUrl = `http://${host}:${Number.isFinite(port) ? port : 3421}`;
|
|
138
|
+
const tokenPath = join(runtime.homeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
|
|
139
|
+
if (!existsSync(tokenPath)) return { baseUrl, token: null, tokenPath };
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(readFileSync(tokenPath, 'utf-8')) as unknown;
|
|
142
|
+
const token = isRecord(parsed) && typeof parsed.token === 'string' ? parsed.token : null;
|
|
143
|
+
return { baseUrl, token, tokenPath };
|
|
144
|
+
} catch {
|
|
145
|
+
return { baseUrl, token: null, tokenPath };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function fetchDaemonStatus(connection: AgentDaemonConnection): Promise<{ readonly ok: boolean; readonly status: number; readonly body: unknown }> {
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch(`${connection.baseUrl}/status`, {
|
|
152
|
+
headers: connection.token ? { authorization: `Bearer ${connection.token}` } : undefined,
|
|
153
|
+
});
|
|
154
|
+
const text = await response.text();
|
|
155
|
+
let body: unknown = text;
|
|
156
|
+
try {
|
|
157
|
+
body = JSON.parse(text) as unknown;
|
|
158
|
+
} catch {
|
|
159
|
+
body = text;
|
|
160
|
+
}
|
|
161
|
+
return { ok: response.ok, status: response.status, body };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return { ok: false, status: 0, body: summarizeError(error) };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function classifyKnowledgeError(error: unknown, connection: AgentDaemonConnection, route: string): AgentKnowledgeFailure {
|
|
168
|
+
const message = summarizeError(error);
|
|
169
|
+
const lower = message.toLowerCase();
|
|
170
|
+
if (lower.includes('401') || lower.includes('unauthorized') || lower.includes('auth')) {
|
|
171
|
+
return { ok: false, kind: 'auth_required', error: message, baseUrl: connection.baseUrl, route };
|
|
172
|
+
}
|
|
173
|
+
if (lower.includes('404') || lower.includes('not found')) {
|
|
174
|
+
return { ok: false, kind: 'daemon_route_unavailable', error: message, baseUrl: connection.baseUrl, route };
|
|
175
|
+
}
|
|
176
|
+
if (lower.includes('fetch') || lower.includes('connect') || lower.includes('econnrefused')) {
|
|
177
|
+
return { ok: false, kind: 'daemon_unavailable', error: message, baseUrl: connection.baseUrl, route };
|
|
178
|
+
}
|
|
179
|
+
return { ok: false, kind: 'daemon_error', error: message, baseUrl: connection.baseUrl, route };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function createAgentSdk(connection: AgentDaemonConnection) {
|
|
183
|
+
return createBrowserAgentSdk({
|
|
184
|
+
baseUrl: connection.baseUrl,
|
|
185
|
+
authToken: connection.token,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function sourceLine(value: unknown): string {
|
|
190
|
+
const record = isRecord(value) ? value : {};
|
|
191
|
+
const title = cleanInline(record.title)
|
|
192
|
+
|| cleanInline(record.canonicalUri)
|
|
193
|
+
|| cleanInline(record.sourceUri)
|
|
194
|
+
|| cleanInline(record.url)
|
|
195
|
+
|| cleanInline(record.id)
|
|
196
|
+
|| 'untitled';
|
|
197
|
+
const type = cleanInline(record.sourceType) || cleanInline(record.type) || 'source';
|
|
198
|
+
const url = cleanInline(record.canonicalUri) || cleanInline(record.sourceUri) || cleanInline(record.url);
|
|
199
|
+
return url && url !== title ? `[${type}] ${title} (${url})` : `[${type}] ${title}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function resultLine(value: unknown): string {
|
|
203
|
+
const record = isRecord(value) ? value : {};
|
|
204
|
+
const title = cleanInline(record.title) || cleanInline(record.id) || 'untitled';
|
|
205
|
+
const id = cleanInline(record.id);
|
|
206
|
+
const type = cleanInline(record.type) || cleanInline(record.kind) || cleanInline(record.sourceType) || 'result';
|
|
207
|
+
const score = readNumber(record, 'score');
|
|
208
|
+
const url = cleanInline(record.url) || cleanInline(record.canonicalUri) || cleanInline(record.sourceUri);
|
|
209
|
+
const snippet = cleanInline(record.snippet) || cleanInline(record.summary) || cleanInline(record.text);
|
|
210
|
+
const parts = [
|
|
211
|
+
`[${type}] ${title}`,
|
|
212
|
+
id && id !== title ? `id=${id}` : '',
|
|
213
|
+
score !== null ? `score=${score.toFixed(3)}` : '',
|
|
214
|
+
url ? `url=${url}` : '',
|
|
215
|
+
].filter((part) => part.length > 0);
|
|
216
|
+
return snippet ? `${parts.join(' ')}\n ${snippet}` : parts.join(' ');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function formatStatus(data: unknown): string {
|
|
220
|
+
const record = isRecord(data) ? data : {};
|
|
221
|
+
const ready = readBoolean(record, 'ready');
|
|
222
|
+
const sourceCount = readNumber(record, 'sourceCount');
|
|
223
|
+
const nodeCount = readNumber(record, 'nodeCount');
|
|
224
|
+
const issueCount = readNumber(record, 'issueCount');
|
|
225
|
+
const edgeCount = readNumber(record, 'edgeCount');
|
|
226
|
+
const storagePath = readString(record, 'storagePath');
|
|
227
|
+
return [
|
|
228
|
+
'Agent Knowledge status',
|
|
229
|
+
` ready: ${ready === null ? 'unknown' : yesNo(ready)}`,
|
|
230
|
+
` sources: ${sourceCount ?? 'unknown'}`,
|
|
231
|
+
` nodes: ${nodeCount ?? 'unknown'}`,
|
|
232
|
+
` edges: ${edgeCount ?? 'unknown'}`,
|
|
233
|
+
` issues: ${issueCount ?? 'unknown'}`,
|
|
234
|
+
storagePath ? ` storage: ${storagePath}` : null,
|
|
235
|
+
' route: /api/goodvibes-agent/knowledge/status',
|
|
236
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function formatAsk(data: unknown, query: string): string {
|
|
240
|
+
const record = isRecord(data) ? data : {};
|
|
241
|
+
const answer = isRecord(record.answer) ? record.answer : record;
|
|
242
|
+
const text = cleanInline(answer.text) || cleanInline(record.answer) || 'No answer returned.';
|
|
243
|
+
const confidence = readNumber(answer, 'confidence') ?? readNumber(record, 'confidence');
|
|
244
|
+
const synthesized = readBoolean(answer, 'synthesized');
|
|
245
|
+
const sources = readArray(answer, 'sources');
|
|
246
|
+
const facts = readArray(answer, 'facts');
|
|
247
|
+
const gaps = readArray(answer, 'gaps');
|
|
248
|
+
const lines = [
|
|
249
|
+
`Agent Knowledge answer: ${query}`,
|
|
250
|
+
text,
|
|
251
|
+
'',
|
|
252
|
+
`confidence: ${confidence ?? 'unknown'}${synthesized === null ? '' : ` synthesized: ${yesNo(synthesized)}`}`,
|
|
253
|
+
];
|
|
254
|
+
if (sources.length > 0) {
|
|
255
|
+
lines.push('', 'Sources:', ...sources.slice(0, 8).map((source) => ` - ${sourceLine(source)}`));
|
|
256
|
+
}
|
|
257
|
+
if (facts.length > 0) lines.push('', `Facts: ${facts.length}`);
|
|
258
|
+
if (gaps.length > 0) lines.push('', `Gaps: ${gaps.length}`);
|
|
259
|
+
return lines.join('\n');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function formatSearch(data: unknown, query: string): string {
|
|
263
|
+
const record = isRecord(data) ? data : {};
|
|
264
|
+
const items = readArray(record, 'items');
|
|
265
|
+
const results = items.length > 0 ? items : readArray(record, 'results');
|
|
266
|
+
if (results.length === 0) {
|
|
267
|
+
return [
|
|
268
|
+
`Agent Knowledge search: ${query}`,
|
|
269
|
+
' no results',
|
|
270
|
+
' route: /api/goodvibes-agent/knowledge/search',
|
|
271
|
+
].join('\n');
|
|
272
|
+
}
|
|
273
|
+
return [
|
|
274
|
+
`Agent Knowledge search: ${query}`,
|
|
275
|
+
...results.slice(0, 10).map((result, index) => ` ${index + 1}. ${resultLine(result)}`),
|
|
276
|
+
].join('\n');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function formatIngest(data: unknown, url: string): string {
|
|
280
|
+
const record = isRecord(data) ? data : {};
|
|
281
|
+
const source = isRecord(record.source) ? record.source : record;
|
|
282
|
+
const sourceId = cleanInline(source.id);
|
|
283
|
+
const canonicalUri = cleanInline(source.canonicalUri) || cleanInline(source.sourceUri) || url;
|
|
284
|
+
const artifactId = cleanInline(record.artifactId);
|
|
285
|
+
return [
|
|
286
|
+
'Agent Knowledge ingest-url accepted',
|
|
287
|
+
` source: ${sourceId || '(pending)'}`,
|
|
288
|
+
` url: ${canonicalUri}`,
|
|
289
|
+
artifactId ? ` artifact: ${artifactId}` : null,
|
|
290
|
+
' route: /api/goodvibes-agent/knowledge/ingest/url',
|
|
291
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function buildDelegationBody(task: string, wrfcRequested: boolean): string {
|
|
295
|
+
return [
|
|
296
|
+
'GoodVibes Agent explicit build delegation.',
|
|
297
|
+
'',
|
|
298
|
+
'Original user ask:',
|
|
299
|
+
task,
|
|
300
|
+
'',
|
|
301
|
+
'Agent policy:',
|
|
302
|
+
'- GoodVibes Agent is not the coding TUI.',
|
|
303
|
+
'- Preserve the full original ask.',
|
|
304
|
+
'- GoodVibes TUI owns file edits, git/worktree flows, sandbox/QEMU UX, and any WRFC owner chain.',
|
|
305
|
+
wrfcRequested
|
|
306
|
+
? '- WRFC was explicitly requested by the Agent user for this build/fix/review delegation.'
|
|
307
|
+
: '- WRFC was not explicitly requested; do not turn this into WRFC solely because it came from Agent.',
|
|
308
|
+
].join('\n');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function formatFailure(failure: AgentKnowledgeFailure, json: boolean): string {
|
|
312
|
+
if (json) return JSON.stringify(failure, null, 2);
|
|
313
|
+
return [
|
|
314
|
+
`Agent Knowledge error: ${failure.kind}`,
|
|
315
|
+
` ${failure.error}`,
|
|
316
|
+
` daemon: ${failure.baseUrl}`,
|
|
317
|
+
` route: ${failure.route}`,
|
|
318
|
+
failure.kind === 'daemon_route_unavailable'
|
|
319
|
+
? ' next: update/restart the external GoodVibes daemon to the SDK version required by this Agent package.'
|
|
320
|
+
: null,
|
|
321
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function runKnowledgeCall<TData>(
|
|
325
|
+
runtime: CliCommandRuntime,
|
|
326
|
+
route: string,
|
|
327
|
+
call: (connection: AgentDaemonConnection) => Promise<TData>,
|
|
328
|
+
): Promise<AgentKnowledgeResult<TData>> {
|
|
329
|
+
const connection = resolveDaemonConnection(runtime);
|
|
330
|
+
if (!connection.token) {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
kind: 'auth_required',
|
|
334
|
+
error: `No daemon operator token found at ${connection.tokenPath}`,
|
|
335
|
+
baseUrl: connection.baseUrl,
|
|
336
|
+
route,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const data = await call(connection);
|
|
341
|
+
return { ok: true, kind: route, route, data };
|
|
342
|
+
} catch (error) {
|
|
343
|
+
return classifyKnowledgeError(error, connection, route);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
348
|
+
const [sub = 'status', ...rest] = runtime.cli.commandArgs;
|
|
349
|
+
const normalized = sub.toLowerCase();
|
|
350
|
+
const json = runtime.cli.flags.outputFormat === 'json';
|
|
351
|
+
|
|
352
|
+
if (normalized === 'status') {
|
|
353
|
+
const result = await runKnowledgeCall(runtime, 'knowledge.status', async (connection) => (
|
|
354
|
+
await createAgentSdk(connection).knowledge.status()
|
|
355
|
+
));
|
|
356
|
+
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
357
|
+
return {
|
|
358
|
+
output: formatJsonOrText(runtime.cli)(result, formatStatus(result.data)),
|
|
359
|
+
exitCode: 0,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (normalized === 'ask') {
|
|
364
|
+
const query = commandValues(rest).join(' ').trim();
|
|
365
|
+
if (!query) return { output: 'Usage: goodvibes-agent knowledge ask <question> [--limit <n>] [--mode concise|standard|detailed]', exitCode: 2 };
|
|
366
|
+
const mode = readOptionValue(rest, '--mode');
|
|
367
|
+
const selectedMode = mode === 'concise' || mode === 'standard' || mode === 'detailed' ? mode : 'standard';
|
|
368
|
+
const limit = readPositiveInt(rest, '--limit', 8);
|
|
369
|
+
const result = await runKnowledgeCall(runtime, 'knowledge.ask', async (connection) => (
|
|
370
|
+
await createAgentSdk(connection).knowledge.ask({
|
|
371
|
+
query,
|
|
372
|
+
limit,
|
|
373
|
+
mode: selectedMode,
|
|
374
|
+
includeSources: true,
|
|
375
|
+
includeConfidence: true,
|
|
376
|
+
includeLinkedObjects: true,
|
|
377
|
+
})
|
|
378
|
+
));
|
|
379
|
+
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
380
|
+
return {
|
|
381
|
+
output: formatJsonOrText(runtime.cli)(result, formatAsk(result.data, query)),
|
|
382
|
+
exitCode: 0,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (normalized === 'search') {
|
|
387
|
+
const query = commandValues(rest).join(' ').trim();
|
|
388
|
+
if (!query) return { output: 'Usage: goodvibes-agent knowledge search <query> [--limit <n>]', exitCode: 2 };
|
|
389
|
+
const limit = readPositiveInt(rest, '--limit', 10);
|
|
390
|
+
const result = await runKnowledgeCall(runtime, 'knowledge.search', async (connection) => (
|
|
391
|
+
await createAgentSdk(connection).knowledge.search({ query, limit })
|
|
392
|
+
));
|
|
393
|
+
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
394
|
+
return {
|
|
395
|
+
output: formatJsonOrText(runtime.cli)(result, formatSearch(result.data, query)),
|
|
396
|
+
exitCode: 0,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (normalized === 'ingest-url') {
|
|
401
|
+
const values = commandValues(rest);
|
|
402
|
+
const url = values[0];
|
|
403
|
+
if (!url) return { output: 'Usage: goodvibes-agent knowledge ingest-url <url> [--title <title>] [--tags a,b]', exitCode: 2 };
|
|
404
|
+
const title = readOptionValue(rest, '--title');
|
|
405
|
+
const tags = readStringList(rest, '--tags');
|
|
406
|
+
const result = await runKnowledgeCall(runtime, 'knowledge.ingest.url', async (connection) => (
|
|
407
|
+
await createAgentSdk(connection).operator.invoke('knowledge.ingest.url', {
|
|
408
|
+
url,
|
|
409
|
+
title,
|
|
410
|
+
tags,
|
|
411
|
+
sourceType: 'url',
|
|
412
|
+
connectorId: 'goodvibes-agent-cli',
|
|
413
|
+
})
|
|
414
|
+
));
|
|
415
|
+
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
416
|
+
return {
|
|
417
|
+
output: formatJsonOrText(runtime.cli)(result, formatIngest(result.data, url)),
|
|
418
|
+
exitCode: 0,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
output: 'Usage: goodvibes-agent knowledge [status|ask <question>|search <query>|ingest-url <url>]',
|
|
424
|
+
exitCode: 2,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export async function handleCompatCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
429
|
+
const connection = resolveDaemonConnection(runtime);
|
|
430
|
+
const metadata = readPackageMetadata();
|
|
431
|
+
const daemon = await fetchDaemonStatus(connection);
|
|
432
|
+
const daemonRecord = isRecord(daemon.body) ? daemon.body : {};
|
|
433
|
+
const daemonVersion = readString(daemonRecord, 'version') ?? 'unknown';
|
|
434
|
+
const versionCompatible = daemon.ok && daemonVersion === metadata.sdkVersion;
|
|
435
|
+
const knowledgeRoute = await runKnowledgeCall(runtime, 'knowledge.status', async (routeConnection) => (
|
|
436
|
+
await createAgentSdk(routeConnection).knowledge.status()
|
|
437
|
+
));
|
|
438
|
+
const knowledgeRouteReady = knowledgeRoute.ok;
|
|
439
|
+
const value = {
|
|
440
|
+
ok: versionCompatible && knowledgeRouteReady,
|
|
441
|
+
packageVersion: metadata.version,
|
|
442
|
+
sdkPin: metadata.sdkVersion,
|
|
443
|
+
daemon: {
|
|
444
|
+
baseUrl: connection.baseUrl,
|
|
445
|
+
status: daemon.status,
|
|
446
|
+
version: daemonVersion,
|
|
447
|
+
reachable: daemon.ok,
|
|
448
|
+
compatible: versionCompatible,
|
|
449
|
+
},
|
|
450
|
+
auth: {
|
|
451
|
+
tokenPresent: Boolean(connection.token),
|
|
452
|
+
tokenPath: connection.tokenPath,
|
|
453
|
+
},
|
|
454
|
+
agentKnowledge: {
|
|
455
|
+
route: '/api/goodvibes-agent/knowledge/status',
|
|
456
|
+
ready: knowledgeRouteReady,
|
|
457
|
+
kind: knowledgeRoute.ok ? 'ok' : knowledgeRoute.kind,
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
const text = [
|
|
461
|
+
'GoodVibes Agent compatibility',
|
|
462
|
+
` package: ${metadata.version}`,
|
|
463
|
+
` SDK pin: ${metadata.sdkVersion}`,
|
|
464
|
+
` daemon: ${daemonVersion} at ${connection.baseUrl} (${daemon.ok ? 'reachable' : 'unreachable'})`,
|
|
465
|
+
` version compatible: ${yesNo(versionCompatible)}`,
|
|
466
|
+
` operator token: ${connection.token ? 'present' : 'missing'} (${connection.tokenPath})`,
|
|
467
|
+
` Agent knowledge route: ${knowledgeRouteReady ? 'ready' : `not ready (${knowledgeRoute.ok ? 'unknown' : knowledgeRoute.kind})`}`,
|
|
468
|
+
...(versionCompatible ? [] : [' next: update/restart the external GoodVibes daemon so /status matches the Agent SDK pin.']),
|
|
469
|
+
].join('\n');
|
|
470
|
+
return {
|
|
471
|
+
output: runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text,
|
|
472
|
+
exitCode: value.ok ? 0 : 1,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export async function handleDelegateCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
477
|
+
const wrfcRequested = hasFlag(runtime.cli.commandArgs, '--wrfc');
|
|
478
|
+
const task = delegationTaskValues(runtime.cli.commandArgs).join(' ').trim();
|
|
479
|
+
if (!task) {
|
|
480
|
+
return {
|
|
481
|
+
output: 'Usage: goodvibes-agent delegate [--wrfc] <build/fix/review task>',
|
|
482
|
+
exitCode: 2,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const result = await runKnowledgeCall(runtime, 'sessions.messages.create', async (connection) => {
|
|
486
|
+
const sdk = createAgentSdk(connection);
|
|
487
|
+
const created = await sdk.operator.invoke('sessions.create', {
|
|
488
|
+
title: `Agent delegation: ${task.slice(0, 72)}`,
|
|
489
|
+
surfaceKind: 'goodvibes-agent',
|
|
490
|
+
surfaceId: 'goodvibes-agent-cli',
|
|
491
|
+
});
|
|
492
|
+
const sessionId = isRecord(created.session) && typeof created.session.id === 'string'
|
|
493
|
+
? created.session.id
|
|
494
|
+
: null;
|
|
495
|
+
if (!sessionId) throw new Error('sessions.create returned no session id.');
|
|
496
|
+
const message = await sdk.operator.invoke('sessions.messages.create', {
|
|
497
|
+
sessionId,
|
|
498
|
+
body: buildDelegationBody(task, wrfcRequested),
|
|
499
|
+
surfaceKind: 'goodvibes-agent',
|
|
500
|
+
surfaceId: 'goodvibes-agent-cli',
|
|
501
|
+
kind: 'task',
|
|
502
|
+
routing: {
|
|
503
|
+
executionIntent: {
|
|
504
|
+
riskClass: 'elevated',
|
|
505
|
+
requiresApproval: true,
|
|
506
|
+
networkPolicy: 'inherit',
|
|
507
|
+
filesystemPolicy: 'workspace-write',
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
return { sessionId, message, task, wrfcRequested };
|
|
512
|
+
});
|
|
513
|
+
if (!result.ok) return { output: formatFailure(result, runtime.cli.flags.outputFormat === 'json'), exitCode: 1 };
|
|
514
|
+
const text = [
|
|
515
|
+
'Delegation submitted to GoodVibes TUI/shared-session routes.',
|
|
516
|
+
` session: ${result.data.sessionId}`,
|
|
517
|
+
` mode: ${result.data.wrfcRequested ? 'WRFC requested' : 'direct build delegation'}`,
|
|
518
|
+
` task: ${result.data.task}`,
|
|
519
|
+
' next: check GoodVibes TUI shared-session/task status for the result.',
|
|
520
|
+
].join('\n');
|
|
521
|
+
return {
|
|
522
|
+
output: formatJsonOrText(runtime.cli)(result, text),
|
|
523
|
+
exitCode: 0,
|
|
524
|
+
};
|
|
525
|
+
}
|
package/src/cli/help.ts
CHANGED
|
@@ -39,6 +39,9 @@ export function renderGoodVibesHelp(binary = 'goodvibes-agent'): string {
|
|
|
39
39
|
' models [provider] List/use/pin selectable models and recent model history',
|
|
40
40
|
' providers List/inspect/use provider config/auth posture',
|
|
41
41
|
' auth Inspect and manage local users, sessions, and bootstrap auth',
|
|
42
|
+
' compat Inspect Agent SDK pin, daemon version, and Agent knowledge route readiness',
|
|
43
|
+
' knowledge Use isolated Agent Knowledge/Wiki routes',
|
|
44
|
+
' delegate Explicitly delegate build/fix/review work to GoodVibes TUI',
|
|
42
45
|
' subscription Start/finish/logout provider subscription sessions',
|
|
43
46
|
' secrets List, set, link, delete, and test GoodVibes secret refs',
|
|
44
47
|
' sessions List, show, export, or resume saved sessions',
|
|
@@ -89,6 +92,10 @@ export function renderGoodVibesHelp(binary = 'goodvibes-agent'): string {
|
|
|
89
92
|
` ${binary} models current`,
|
|
90
93
|
` ${binary} models use openai:gpt-5.2`,
|
|
91
94
|
` ${binary} providers inspect openai`,
|
|
95
|
+
` ${binary} compat`,
|
|
96
|
+
` ${binary} knowledge status`,
|
|
97
|
+
` ${binary} knowledge ask "What is GoodVibes Agent?"`,
|
|
98
|
+
` ${binary} delegate --wrfc "fix the failing tests in ~/work/project"`,
|
|
92
99
|
` ${binary} surfaces`,
|
|
93
100
|
` ${binary} surfaces check`,
|
|
94
101
|
` ${binary} service check`,
|
|
@@ -147,6 +154,34 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
147
154
|
summary: 'Inspect and manage local admin users, bootstrap auth, and local sessions.',
|
|
148
155
|
examples: ['auth', 'auth add-user admin --password-stdin', 'auth clear-bootstrap'],
|
|
149
156
|
},
|
|
157
|
+
compat: {
|
|
158
|
+
usage: ['compat', 'compat --json'],
|
|
159
|
+
summary: 'Inspect package SDK pin, live daemon version, and Agent-specific knowledge route readiness.',
|
|
160
|
+
examples: ['compat', 'compat --json'],
|
|
161
|
+
},
|
|
162
|
+
knowledge: {
|
|
163
|
+
usage: [
|
|
164
|
+
'knowledge status',
|
|
165
|
+
'knowledge ask <question> [--limit <n>] [--mode concise|standard|detailed]',
|
|
166
|
+
'knowledge search <query> [--limit <n>]',
|
|
167
|
+
'knowledge ingest-url <url> [--title <title>] [--tags a,b]',
|
|
168
|
+
],
|
|
169
|
+
summary: 'Call isolated Agent Knowledge/Wiki routes under /api/goodvibes-agent/knowledge. No default wiki or HomeGraph fallback.',
|
|
170
|
+
examples: [
|
|
171
|
+
'knowledge status',
|
|
172
|
+
'knowledge ask "What is GoodVibes Agent?"',
|
|
173
|
+
'knowledge search "release checklist"',
|
|
174
|
+
'knowledge ingest-url https://example.com/page --title "Reference"',
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
delegate: {
|
|
178
|
+
usage: ['delegate [--wrfc] <build/fix/review task>'],
|
|
179
|
+
summary: 'Create one shared-session task request for GoodVibes TUI. WRFC is requested only with --wrfc.',
|
|
180
|
+
examples: [
|
|
181
|
+
'delegate "fix the failing tests in this repo"',
|
|
182
|
+
'delegate --wrfc "implement the settings screen and review it"',
|
|
183
|
+
],
|
|
184
|
+
},
|
|
150
185
|
subscription: {
|
|
151
186
|
usage: ['subscription list', 'subscription providers', 'subscription inspect <provider>', 'subscription login <provider> start|finish', 'subscription logout <provider>'],
|
|
152
187
|
summary: 'Manage OAuth/subscription-backed provider sessions such as OpenAI subscription access.',
|
package/src/cli/management.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibe
|
|
|
21
21
|
import { inspectProviderAuth } from '@/runtime/index.ts';
|
|
22
22
|
import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnectionPayload, formatConnectionBlock } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
23
23
|
import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
24
|
+
import { UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
|
|
24
25
|
import type { GoodVibesCliParseResult } from './types.ts';
|
|
25
26
|
import { formatProviderAuthRoute, summarizeProviderAuthRoutes } from './provider-auth-routes.ts';
|
|
26
27
|
import { classifyProviderSetup } from './provider-classification.ts';
|
|
@@ -31,6 +32,7 @@ import { handleServiceCommand } from './service-command.ts';
|
|
|
31
32
|
import { handleBundleCommand } from './bundle-command.ts';
|
|
32
33
|
import { buildListenerTestResult, formatListenerTestResult, handleSurfacesCommand } from './surface-command.ts';
|
|
33
34
|
import { buildControlPlaneStatusResult, formatControlPlaneStatus, handleSecrets, handleSessions, handleTasks, renderPairing, renderRemote, renderSubscriptions, renderWeb } from './management-commands.ts';
|
|
35
|
+
import { handleAgentKnowledgeCommand, handleCompatCommand, handleDelegateCommand } from './agent-knowledge-command.ts';
|
|
34
36
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
35
37
|
|
|
36
38
|
export interface CliCommandRuntime {
|
|
@@ -289,6 +291,14 @@ export function readAuthPaths(runtime: CliCommandRuntime) {
|
|
|
289
291
|
};
|
|
290
292
|
}
|
|
291
293
|
|
|
294
|
+
function createCliLocalUserAuthManager(runtime: CliCommandRuntime): UserAuthManager {
|
|
295
|
+
const paths = readAuthPaths(runtime);
|
|
296
|
+
return new UserAuthManager({
|
|
297
|
+
bootstrapFilePath: paths.userStorePath,
|
|
298
|
+
bootstrapCredentialPath: paths.bootstrapCredentialPath,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
292
302
|
export async function runNonInteractiveAgent(runtime: CliCommandRuntime): Promise<number> {
|
|
293
303
|
const prompt = runtime.cli.flags.prompt ?? runtime.cli.positionals.join(' ').trim();
|
|
294
304
|
if (!prompt) {
|
|
@@ -574,7 +584,7 @@ async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
|
574
584
|
}
|
|
575
585
|
|
|
576
586
|
async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
|
|
577
|
-
|
|
587
|
+
const localUserAuthManager = createCliLocalUserAuthManager(runtime);
|
|
578
588
|
const [sub = 'status', ...rawRest] = runtime.cli.commandArgs;
|
|
579
589
|
const rest = commandValues(rawRest);
|
|
580
590
|
if (sub === 'add-user' || sub === 'add') {
|
|
@@ -583,13 +593,13 @@ async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
|
|
|
583
593
|
const password = readPassword(rawRest);
|
|
584
594
|
if (!password) return 'Usage: goodvibes auth add-user <username> [--password <value>|--password-stdin] [--role <role>]';
|
|
585
595
|
const roles = readOptionValues(rawRest, '--role').filter((role) => role.length > 0);
|
|
586
|
-
const user =
|
|
596
|
+
const user = localUserAuthManager.addUser(username, password, roles.length > 0 ? roles : ['user']);
|
|
587
597
|
return `Auth user added: ${user.username} (${user.roles.join(', ') || 'no roles'})`;
|
|
588
598
|
}
|
|
589
599
|
if (sub === 'delete-user' || sub === 'remove-user') {
|
|
590
600
|
const username = rest[0];
|
|
591
601
|
if (!username) return 'Usage: goodvibes auth delete-user <username>';
|
|
592
|
-
return
|
|
602
|
+
return localUserAuthManager.deleteUser(username)
|
|
593
603
|
? `Auth user deleted: ${username}`
|
|
594
604
|
: `No auth user found: ${username}`;
|
|
595
605
|
}
|
|
@@ -598,31 +608,31 @@ async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
|
|
|
598
608
|
if (!username) return 'Usage: goodvibes auth rotate-password <username> [--password <value>|--password-stdin]';
|
|
599
609
|
const password = readPassword(rawRest);
|
|
600
610
|
if (!password) return 'Usage: goodvibes auth rotate-password <username> [--password <value>|--password-stdin]';
|
|
601
|
-
|
|
611
|
+
localUserAuthManager.rotatePassword(username, password);
|
|
602
612
|
return `Auth password rotated: ${username}`;
|
|
603
613
|
}
|
|
604
614
|
if (sub === 'revoke-session') {
|
|
605
615
|
const token = rest[0];
|
|
606
616
|
if (!token) return 'Usage: goodvibes auth revoke-session <token-or-fingerprint>';
|
|
607
|
-
return
|
|
617
|
+
return localUserAuthManager.revokeSession(token)
|
|
608
618
|
? 'Auth session revoked.'
|
|
609
619
|
: 'No auth session found.';
|
|
610
620
|
}
|
|
611
621
|
if (sub === 'revoke-sessions') {
|
|
612
622
|
const username = rest[0];
|
|
613
623
|
if (!username) return 'Usage: goodvibes auth revoke-sessions <username>';
|
|
614
|
-
const count =
|
|
624
|
+
const count = localUserAuthManager.revokeSessionsForUser(username);
|
|
615
625
|
return `Auth sessions revoked for ${username}: ${count}`;
|
|
616
626
|
}
|
|
617
627
|
if (sub === 'clear-bootstrap') {
|
|
618
|
-
return
|
|
628
|
+
return localUserAuthManager.clearBootstrapCredentialFile()
|
|
619
629
|
? 'Bootstrap credential file removed.'
|
|
620
630
|
: 'Bootstrap credential file was already absent.';
|
|
621
631
|
}
|
|
622
632
|
if (sub !== 'status' && sub !== 'list' && sub !== 'users' && sub !== 'sessions') {
|
|
623
633
|
return 'Usage: goodvibes auth [status|users|sessions|add-user|delete-user|rotate-password|revoke-session|revoke-sessions|clear-bootstrap]';
|
|
624
634
|
}
|
|
625
|
-
const snapshot =
|
|
635
|
+
const snapshot = localUserAuthManager.inspect();
|
|
626
636
|
const paths = readAuthPaths(runtime);
|
|
627
637
|
const value = {
|
|
628
638
|
...paths,
|
|
@@ -652,7 +662,6 @@ async function renderAuth(runtime: CliCommandRuntime): Promise<string> {
|
|
|
652
662
|
` bootstrap credential: ${paths.bootstrapCredentialPresent ? 'present' : 'missing'} (${paths.bootstrapCredentialPath})`,
|
|
653
663
|
` operator tokens: ${paths.operatorTokenPresent ? 'present' : 'missing'} (${paths.operatorTokenPath})`,
|
|
654
664
|
].join('\n'));
|
|
655
|
-
});
|
|
656
665
|
}
|
|
657
666
|
|
|
658
667
|
|
|
@@ -684,6 +693,21 @@ export async function handleGoodVibesCliCommand(runtime: CliCommandRuntime): Pro
|
|
|
684
693
|
console.log(output);
|
|
685
694
|
return { handled: true, exitCode: exitCodeForText(output) };
|
|
686
695
|
}
|
|
696
|
+
case 'compat': {
|
|
697
|
+
const result = await handleCompatCommand(runtime);
|
|
698
|
+
console.log(result.output);
|
|
699
|
+
return { handled: true, exitCode: result.exitCode };
|
|
700
|
+
}
|
|
701
|
+
case 'knowledge': {
|
|
702
|
+
const result = await handleAgentKnowledgeCommand(runtime);
|
|
703
|
+
console.log(result.output);
|
|
704
|
+
return { handled: true, exitCode: result.exitCode };
|
|
705
|
+
}
|
|
706
|
+
case 'delegate': {
|
|
707
|
+
const result = await handleDelegateCommand(runtime);
|
|
708
|
+
console.log(result.output);
|
|
709
|
+
return { handled: true, exitCode: result.exitCode };
|
|
710
|
+
}
|
|
687
711
|
case 'subscription': {
|
|
688
712
|
const output = await renderSubscriptions(runtime);
|
|
689
713
|
console.log(output);
|
package/src/cli/parser.ts
CHANGED
|
@@ -26,6 +26,13 @@ const COMMAND_ALIASES: Readonly<Record<string, GoodVibesCliCommand>> = {
|
|
|
26
26
|
providers: 'providers',
|
|
27
27
|
provider: 'providers',
|
|
28
28
|
auth: 'auth',
|
|
29
|
+
compat: 'compat',
|
|
30
|
+
compatibility: 'compat',
|
|
31
|
+
knowledge: 'knowledge',
|
|
32
|
+
know: 'knowledge',
|
|
33
|
+
kb: 'knowledge',
|
|
34
|
+
delegate: 'delegate',
|
|
35
|
+
build: 'delegate',
|
|
29
36
|
subscription: 'subscription',
|
|
30
37
|
subscriptions: 'subscription',
|
|
31
38
|
secrets: 'secrets',
|
package/src/cli/types.ts
CHANGED
|
@@ -203,7 +203,7 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
|
|
|
203
203
|
detail: 'Agent does not become the coding TUI. Build, implement, fix, patch, and review work must be handed to GoodVibes TUI with the full original ask and WRFC only when explicitly requested.',
|
|
204
204
|
actions: [
|
|
205
205
|
{ id: 'delegate-guidance', label: 'Delegation rule', detail: 'For build/fix/review work, delegate one request to GoodVibes TUI instead of spawning local Engineer/Reviewer/Tester roots.', kind: 'guidance', safety: 'delegates' },
|
|
206
|
-
{ id: 'review-command', label: 'Review delegation command', detail: 'Use /
|
|
206
|
+
{ id: 'review-command', label: 'Review delegation command', detail: 'Use /delegate --wrfc only when the user explicitly asks for code review/build execution.', command: '/delegate --wrfc <task>', kind: 'command', safety: 'delegates' },
|
|
207
207
|
{ id: 'remote-policy', label: 'Remote runner policy', detail: 'Remote dispatch/rerun is blocked in Agent; TUI owns runner topology for delegated build work.', command: '/remote dispatch', kind: 'command', safety: 'blocked' },
|
|
208
208
|
],
|
|
209
209
|
},
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
2
|
+
import type { SharedSessionParticipant } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
3
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
|
+
|
|
5
|
+
function hasFlag(args: readonly string[], flag: string): boolean {
|
|
6
|
+
return args.includes(flag);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function delegationTaskValues(args: readonly string[]): string[] {
|
|
10
|
+
const values: string[] = [];
|
|
11
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
12
|
+
const token = args[index]!;
|
|
13
|
+
if (token === '--wrfc') continue;
|
|
14
|
+
if (!token.startsWith('--')) {
|
|
15
|
+
values.push(token);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return values;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildDelegationBody(task: string, wrfcRequested: boolean): string {
|
|
23
|
+
return [
|
|
24
|
+
'GoodVibes Agent explicit build delegation.',
|
|
25
|
+
'',
|
|
26
|
+
'Original user ask:',
|
|
27
|
+
task,
|
|
28
|
+
'',
|
|
29
|
+
'Agent policy:',
|
|
30
|
+
'- GoodVibes Agent is not the coding TUI.',
|
|
31
|
+
'- Preserve the full original ask.',
|
|
32
|
+
'- GoodVibes TUI owns file edits, git/worktree flows, sandbox/QEMU UX, and any WRFC owner chain.',
|
|
33
|
+
wrfcRequested
|
|
34
|
+
? '- WRFC was explicitly requested by the Agent user for this build/fix/review delegation.'
|
|
35
|
+
: '- WRFC was not explicitly requested; do not turn this into WRFC solely because it came from Agent.',
|
|
36
|
+
].join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function registerDelegationRuntimeCommands(registry: CommandRegistry): void {
|
|
40
|
+
const makeHandler = (defaultWrfc: boolean) => async (args: string[], ctx: CommandContext): Promise<void> => {
|
|
41
|
+
const wrfcRequested = defaultWrfc || hasFlag(args, '--wrfc');
|
|
42
|
+
const task = delegationTaskValues(args).join(' ').trim();
|
|
43
|
+
if (!task) {
|
|
44
|
+
ctx.print(defaultWrfc ? 'Usage: /wrfc <build/fix/review task>' : 'Usage: /delegate [--wrfc] <build/fix/review task>');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const operator = ctx.clients?.operator;
|
|
48
|
+
if (!operator) {
|
|
49
|
+
ctx.print([
|
|
50
|
+
'Delegation unavailable: no operator client is attached.',
|
|
51
|
+
'Use the external daemon/shared-session route from a configured Agent runtime, or open GoodVibes TUI in the target workspace.',
|
|
52
|
+
].join('\n'));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const participant = {
|
|
57
|
+
surfaceKind: 'service',
|
|
58
|
+
surfaceId: 'goodvibes-agent',
|
|
59
|
+
externalId: ctx.session.runtime.sessionId,
|
|
60
|
+
displayName: 'GoodVibes Agent',
|
|
61
|
+
lastSeenAt: Date.now(),
|
|
62
|
+
} satisfies SharedSessionParticipant;
|
|
63
|
+
const session = await operator.sessions.ensureSession({
|
|
64
|
+
title: `Agent delegation: ${task.slice(0, 72)}`,
|
|
65
|
+
participant,
|
|
66
|
+
metadata: {
|
|
67
|
+
originSurface: 'goodvibes-agent',
|
|
68
|
+
sourceSessionId: ctx.session.runtime.sessionId,
|
|
69
|
+
task,
|
|
70
|
+
wrfcRequested,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
await operator.sessions.submitMessage({
|
|
74
|
+
sessionId: session.id,
|
|
75
|
+
body: buildDelegationBody(task, wrfcRequested),
|
|
76
|
+
surfaceKind: participant.surfaceKind,
|
|
77
|
+
surfaceId: participant.surfaceId,
|
|
78
|
+
externalId: participant.externalId,
|
|
79
|
+
displayName: participant.displayName,
|
|
80
|
+
title: `Agent delegation: ${task.slice(0, 72)}`,
|
|
81
|
+
metadata: {
|
|
82
|
+
originSurface: 'goodvibes-agent',
|
|
83
|
+
sourceSessionId: ctx.session.runtime.sessionId,
|
|
84
|
+
kind: 'task',
|
|
85
|
+
task,
|
|
86
|
+
wrfcRequested,
|
|
87
|
+
},
|
|
88
|
+
routing: {
|
|
89
|
+
executionIntent: {
|
|
90
|
+
riskClass: 'elevated',
|
|
91
|
+
requiresApproval: true,
|
|
92
|
+
networkPolicy: 'inherit',
|
|
93
|
+
filesystemPolicy: 'workspace-write',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
ctx.print([
|
|
98
|
+
'Delegation submitted to GoodVibes TUI/shared-session routes.',
|
|
99
|
+
` session: ${session.id}`,
|
|
100
|
+
` mode: ${wrfcRequested ? 'WRFC requested' : 'direct build delegation'}`,
|
|
101
|
+
` task: ${task}`,
|
|
102
|
+
' next: check GoodVibes TUI shared-session/task status for the result.',
|
|
103
|
+
].join('\n'));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
ctx.print([
|
|
106
|
+
'Delegation failed.',
|
|
107
|
+
` error: ${summarizeError(error)}`,
|
|
108
|
+
' fallback: open GoodVibes TUI in the target workspace and paste the original task there.',
|
|
109
|
+
].join('\n'));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
registry.register({
|
|
114
|
+
name: 'delegate',
|
|
115
|
+
aliases: ['build'],
|
|
116
|
+
description: 'Explicitly delegate build/fix/review work to GoodVibes TUI through shared-session routes',
|
|
117
|
+
usage: '[--wrfc] <task>',
|
|
118
|
+
argsHint: '[--wrfc] <task>',
|
|
119
|
+
handler: makeHandler(false),
|
|
120
|
+
});
|
|
121
|
+
registry.register({
|
|
122
|
+
name: 'wrfc',
|
|
123
|
+
aliases: ['review'],
|
|
124
|
+
description: 'Explicitly delegate build/fix/review work to GoodVibes TUI with WRFC requested',
|
|
125
|
+
usage: '<task>',
|
|
126
|
+
argsHint: '<task>',
|
|
127
|
+
handler: makeHandler(true),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -2,7 +2,6 @@ import type { CommandRegistry } from '../command-registry.ts';
|
|
|
2
2
|
import type { SelectionItem } from '../selection-modal.ts';
|
|
3
3
|
import { EFFORT_DESCRIPTIONS } from '@pellux/goodvibes-sdk/platform/providers';
|
|
4
4
|
import { REASONING_BUDGET_MAP } from '@pellux/goodvibes-sdk/platform/providers';
|
|
5
|
-
import { executeWriteQuit } from './quit-shared.ts';
|
|
6
5
|
import { compactConversation, requireKeybindingsManager, requireProviderApi } from './runtime-services.ts';
|
|
7
6
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
8
7
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
@@ -159,7 +158,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
159
158
|
{ id: '/secrets', label: '/secrets set|link|get|test|list|delete', detail: 'Manage encrypted and provider-backed secrets', category: 'Tools & System' },
|
|
160
159
|
{ id: '/help', label: '/help', detail: 'This help', category: 'Tools & System' },
|
|
161
160
|
{ id: '/quit', label: '/quit', detail: 'Exit', category: 'Tools & System' },
|
|
162
|
-
{ id: '/wq', label: '/wq', detail: '
|
|
161
|
+
{ id: '/wq', label: '/wq', detail: 'Blocked in Agent; git commit/exit belongs to GoodVibes TUI', category: 'Tools & System' },
|
|
163
162
|
];
|
|
164
163
|
ctx.openSelection('Help — Commands', items, { allowSearch: true }, (result) => {
|
|
165
164
|
if (!result) return;
|
|
@@ -173,7 +172,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
173
172
|
});
|
|
174
173
|
return;
|
|
175
174
|
}
|
|
176
|
-
ctx.print('Use /help to open the help modal. Commands: /agent, /model, /provider, /config, /template, /tools, /paste, /sessions, /bookmarks, /save, /load, /undo, /redo, /retry, /clear, /reset, /compact, /export, /title, /effort, /expand, /collapse, /debug, /quit
|
|
175
|
+
ctx.print('Use /help to open the help modal. Commands: /agent, /model, /provider, /config, /template, /tools, /paste, /sessions, /bookmarks, /save, /load, /undo, /redo, /retry, /clear, /reset, /compact, /export, /title, /effort, /expand, /collapse, /debug, /quit');
|
|
177
176
|
},
|
|
178
177
|
});
|
|
179
178
|
|
|
@@ -225,9 +224,13 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
225
224
|
registry.register({
|
|
226
225
|
name: 'wq',
|
|
227
226
|
aliases: [':wq'],
|
|
228
|
-
description: '
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
description: 'Blocked in Agent; git commit/exit is owned by GoodVibes TUI',
|
|
228
|
+
handler(_args, ctx) {
|
|
229
|
+
ctx.print([
|
|
230
|
+
'Blocked: /wq is not available in GoodVibes Agent.',
|
|
231
|
+
'Git commit, worktree, and coding-session exit flows belong to GoodVibes TUI.',
|
|
232
|
+
'No files, commits, or repository state were changed.',
|
|
233
|
+
].join('\n'));
|
|
231
234
|
},
|
|
232
235
|
});
|
|
233
236
|
|
package/src/input/commands.ts
CHANGED
|
@@ -54,6 +54,7 @@ import { registerCloudflareRuntimeCommands } from './commands/cloudflare-runtime
|
|
|
54
54
|
import { registerWorkPlanRuntimeCommands } from './commands/work-plan-runtime.ts';
|
|
55
55
|
import { registerAgentWorkspaceRuntimeCommands } from './commands/agent-workspace-runtime.ts';
|
|
56
56
|
import { registerAgentExternalizedTuiCommands } from './commands/agent-externalized-tui.ts';
|
|
57
|
+
import { registerDelegationRuntimeCommands } from './commands/delegation-runtime.ts';
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
60
|
* registerBuiltinCommands - Register all built-in slash commands into the registry.
|
|
@@ -62,6 +63,7 @@ import { registerAgentExternalizedTuiCommands } from './commands/agent-externali
|
|
|
62
63
|
export function registerBuiltinCommands(registry: CommandRegistry): void {
|
|
63
64
|
registerShellCoreCommands(registry);
|
|
64
65
|
registerAgentWorkspaceRuntimeCommands(registry);
|
|
66
|
+
registerDelegationRuntimeCommands(registry);
|
|
65
67
|
registerConfigCommand(registry);
|
|
66
68
|
registerOperatorRuntimeCommands(registry);
|
|
67
69
|
registerIntegrationRuntimeCommands(registry);
|
|
@@ -8,7 +8,7 @@ export interface SubmissionRouterInput {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const PLAN_COMMANDS = new Set(['plan']);
|
|
11
|
-
const DELEGATION_COMMANDS = new Set(['review', 'wrfc', 'teamwork', 'agents', 'remote']);
|
|
11
|
+
const DELEGATION_COMMANDS = new Set(['delegate', 'build', 'review', 'wrfc', 'teamwork', 'agents', 'remote']);
|
|
12
12
|
const PANEL_COMMANDS = new Set(['panel']);
|
|
13
13
|
const ORCHESTRATION_COMMANDS = new Set([
|
|
14
14
|
'orchestration',
|
|
@@ -3,7 +3,6 @@ import { AgentLogsPanel } from '../agent-logs-panel.ts';
|
|
|
3
3
|
import { ContextVisualizerPanel } from '../context-visualizer-panel.ts';
|
|
4
4
|
import { ThinkingPanel } from '../thinking-panel.ts';
|
|
5
5
|
import { ToolInspectorPanel } from '../tool-inspector-panel.ts';
|
|
6
|
-
import { WrfcPanel } from '../wrfc-panel.ts';
|
|
7
6
|
import { SchedulePanel } from '../schedule-panel.ts';
|
|
8
7
|
import { ProjectPlanningPanel } from '../project-planning-panel.ts';
|
|
9
8
|
import { WorkPlanPanel } from '../work-plan-panel.ts';
|
|
@@ -67,19 +66,6 @@ export function registerAgentPanels(manager: PanelManager, deps: ResolvedBuiltin
|
|
|
67
66
|
},
|
|
68
67
|
});
|
|
69
68
|
|
|
70
|
-
manager.registerType({
|
|
71
|
-
id: 'wrfc',
|
|
72
|
-
name: 'WRFC',
|
|
73
|
-
icon: 'W',
|
|
74
|
-
category: 'agent',
|
|
75
|
-
description: 'WRFC chain view: write, review, fix, and confirm cycle status',
|
|
76
|
-
preload: true,
|
|
77
|
-
factory: () => {
|
|
78
|
-
const ui = requireUiServices(deps);
|
|
79
|
-
return new WrfcPanel(ui.events.workflows, { controller: ui.agents.wrfcController });
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
|
|
83
69
|
manager.registerType({
|
|
84
70
|
id: 'work-plan',
|
|
85
71
|
name: 'Work Plan',
|
|
@@ -33,6 +33,7 @@ import type { TaskManager } from '@/runtime/index.ts';
|
|
|
33
33
|
import type { UiRuntimeServices } from './ui-services.ts';
|
|
34
34
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
35
35
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
36
|
+
import { createKnowledgeApi } from '@pellux/goodvibes-sdk/platform/knowledge';
|
|
36
37
|
|
|
37
38
|
export interface BootstrapShellState {
|
|
38
39
|
readonly commandRegistry: CommandRegistry;
|
|
@@ -208,7 +209,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
208
209
|
memoryRegistry: services.memoryRegistry,
|
|
209
210
|
integrationHelpers: services.integrationHelpers,
|
|
210
211
|
automationManager: services.automationManager,
|
|
211
|
-
knowledgeService: services.
|
|
212
|
+
knowledgeService: services.agentKnowledgeService,
|
|
212
213
|
projectPlanningService: services.projectPlanningService,
|
|
213
214
|
projectPlanningProjectId: services.projectPlanningProjectId,
|
|
214
215
|
workPlanStore: services.workPlanStore,
|
|
@@ -238,7 +239,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
238
239
|
sessionOrchestration: services.sessionOrchestration,
|
|
239
240
|
operatorClient: directTransport.operator,
|
|
240
241
|
peerClient: directTransport.peer,
|
|
241
|
-
knowledgeApi,
|
|
242
|
+
knowledgeApi: createKnowledgeApi(services.agentKnowledgeService, { memoryRegistry: services.memoryRegistry }),
|
|
242
243
|
hookApi,
|
|
243
244
|
mcpApi,
|
|
244
245
|
opsApi,
|
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.
|
|
9
|
+
let _version = '0.1.2';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Michael Davis and GoodVibes contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|