@pellux/goodvibes-agent 0.1.1 → 0.1.3
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 +14 -0
- package/README.md +12 -1
- package/docs/README.md +2 -0
- package/docs/getting-started.md +19 -1
- package/docs/release-and-publishing.md +3 -1
- package/package.json +10 -1
- package/src/agent/persona-registry.ts +379 -0
- package/src/agent/skill-registry.ts +360 -0
- package/src/audio/spoken-turn-model-routing.ts +2 -1
- package/src/cli/agent-knowledge-command.ts +525 -0
- package/src/cli/help.ts +35 -0
- package/src/cli/management-commands.ts +3 -1
- package/src/cli/management.ts +33 -9
- package/src/cli/parser.ts +7 -0
- package/src/cli/types.ts +3 -0
- package/src/config/surface.ts +1 -0
- package/src/input/agent-workspace.ts +33 -3
- package/src/input/command-registry.ts +4 -1
- package/src/input/commands/agent-skills-runtime.ts +216 -0
- package/src/input/commands/delegation-runtime.ts +129 -0
- package/src/input/commands/knowledge.ts +18 -18
- package/src/input/commands/personas-runtime.ts +219 -0
- package/src/input/commands/shell-core.ts +9 -6
- package/src/input/commands/skills-runtime.ts +7 -2
- package/src/input/commands.ts +6 -0
- package/src/input/panel-integration-actions.ts +0 -52
- package/src/input/submission-router.ts +1 -1
- package/src/main.ts +2 -1
- package/src/panels/builtin/agent.ts +0 -14
- package/src/panels/builtin/session.ts +4 -3
- package/src/panels/index.ts +0 -5
- package/src/panels/orchestration-panel.ts +4 -5
- package/src/panels/qr-panel.ts +3 -2
- package/src/panels/tasks-panel.ts +4 -4
- package/src/renderer/agent-workspace.ts +2 -0
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +6 -2
- package/src/runtime/bootstrap-core.ts +8 -4
- package/src/runtime/bootstrap-shell.ts +5 -2
- package/src/runtime/bootstrap.ts +10 -2
- package/src/runtime/cloudflare-control-plane.ts +2 -1
- package/src/version.ts +1 -1
- package/src/daemon/cli.ts +0 -55
- package/src/daemon/safe-serve.ts +0 -61
- package/src/panels/diff-panel.ts +0 -520
- package/src/panels/file-explorer-panel.ts +0 -584
- package/src/panels/file-preview-panel.ts +0 -434
- package/src/panels/git-panel.ts +0 -638
- package/src/panels/sandbox-panel.ts +0 -283
- package/src/panels/symbol-outline-panel.ts +0 -486
- package/src/panels/worktree-panel.ts +0 -182
- package/src/panels/wrfc-panel.ts +0 -609
|
@@ -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.',
|
|
@@ -11,6 +11,7 @@ import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
|
11
11
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
12
12
|
import type { CliCommandRuntime } from './management.ts';
|
|
13
13
|
import { extractAuthorizationCode, formatJsonOrText, hasCommandFlag, openBrowser, probeTcp, readAuthPaths, runNonInteractiveAgent, urlHostForBindHost, withRuntimeServices, yesNo } from './management.ts';
|
|
14
|
+
import { GOODVIBES_AGENT_PAIRING_SURFACE } from '../config/surface.ts';
|
|
14
15
|
|
|
15
16
|
export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<string> {
|
|
16
17
|
return await withRuntimeServices(runtime, async (services) => {
|
|
@@ -370,13 +371,14 @@ export async function renderControlPlaneStatus(runtime: CliCommandRuntime): Prom
|
|
|
370
371
|
|
|
371
372
|
export async function renderPairing(runtime: CliCommandRuntime): Promise<string> {
|
|
372
373
|
const daemonHomeDir = join(runtime.homeDirectory, '.goodvibes', 'daemon');
|
|
373
|
-
const tokenRecord = getOrCreateCompanionToken(
|
|
374
|
+
const tokenRecord = getOrCreateCompanionToken(GOODVIBES_AGENT_PAIRING_SURFACE, { daemonHomeDir });
|
|
374
375
|
const binding = resolveRuntimeEndpointBinding(runtime.configManager, 'controlPlane');
|
|
375
376
|
const daemonUrl = `http://${urlHostForBindHost(binding.host)}:${binding.port}`;
|
|
376
377
|
const info = buildCompanionConnectionInfo({
|
|
377
378
|
daemonUrl,
|
|
378
379
|
token: tokenRecord.token,
|
|
379
380
|
username: 'admin',
|
|
381
|
+
surface: GOODVIBES_AGENT_PAIRING_SURFACE,
|
|
380
382
|
});
|
|
381
383
|
const payload = encodeConnectionPayload(info);
|
|
382
384
|
const qr = renderQrToString(generateQrMatrix(payload));
|