@pellux/goodvibes-agent 0.1.90 → 0.1.92
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/README.md +4 -0
- package/package.json +1 -1
- package/src/cli/agent-knowledge-command.ts +212 -144
- package/src/cli/agent-knowledge-format.ts +325 -0
- package/src/cli/bundle-command.ts +3 -3
- package/src/cli/help.ts +10 -1
- package/src/cli/management-commands.ts +19 -17
- package/src/cli/management.ts +8 -6
- package/src/version.ts +1 -1
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.92 - 2026-06-01
|
|
6
|
+
|
|
7
|
+
- f52b62e Fix copied CLI executable guidance
|
|
8
|
+
|
|
9
|
+
## 0.1.91 - 2026-06-01
|
|
10
|
+
|
|
11
|
+
- efbb82a Expand Agent Knowledge CLI management
|
|
12
|
+
|
|
5
13
|
## 0.1.90 - 2026-06-01
|
|
6
14
|
|
|
7
15
|
- 19f67ea Polish Agent command guidance
|
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ goodvibes-agent --help
|
|
|
17
17
|
goodvibes-agent status
|
|
18
18
|
goodvibes-agent profiles templates
|
|
19
19
|
goodvibes-agent knowledge status
|
|
20
|
+
goodvibes-agent knowledge list --kind sources
|
|
21
|
+
goodvibes-agent knowledge import-urls ./agent-sources.txt --yes
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
If `goodvibes-agent` is not found after installation, add Bun's global bin directory to `PATH`:
|
|
@@ -95,6 +97,8 @@ GoodVibes Agent owns the operator assistant TUI: serial assistant flow, proactiv
|
|
|
95
97
|
|
|
96
98
|
Agent Knowledge/Wiki is its own product segment. Agent uses `/api/goodvibes-agent/knowledge/*` and must not fall back to default Knowledge/Wiki or other product-specific knowledge routes.
|
|
97
99
|
|
|
100
|
+
Agent Knowledge CLI commands can ask/search, inspect sources/nodes/issues, inspect connectors, ingest a URL, import URL/bookmark files, and reindex the Agent segment. Confirmed mutations require `--yes`.
|
|
101
|
+
|
|
98
102
|
GoodVibes TUI owns coding execution: file edits, git/worktree workflows, coding panels, execution isolation UX, and WRFC execution. Agent may delegate explicit build/fix/review work to TUI through public runtime/session contracts; normal assistant chat must not use shared coding sessions.
|
|
99
103
|
|
|
100
104
|
## Package Docs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.92",
|
|
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",
|
|
@@ -5,6 +5,19 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
5
5
|
import { SDK_VERSION, VERSION } from '../version.ts';
|
|
6
6
|
import type { CliCommandOutput } from './types.ts';
|
|
7
7
|
import type { CliCommandRuntime } from './management.ts';
|
|
8
|
+
import {
|
|
9
|
+
formatAsk,
|
|
10
|
+
formatBatchIngest,
|
|
11
|
+
formatConnectors,
|
|
12
|
+
formatEntityList,
|
|
13
|
+
formatFailure,
|
|
14
|
+
formatIngest,
|
|
15
|
+
formatItem,
|
|
16
|
+
formatMap,
|
|
17
|
+
formatReindex,
|
|
18
|
+
formatSearch,
|
|
19
|
+
formatStatus,
|
|
20
|
+
} from './agent-knowledge-format.ts';
|
|
8
21
|
import { formatJsonOrText, yesNo } from './management.ts';
|
|
9
22
|
|
|
10
23
|
type JsonRecord = Record<string, unknown>;
|
|
@@ -52,10 +65,46 @@ const AGENT_KNOWLEDGE_METHODS = {
|
|
|
52
65
|
kind: 'agentKnowledge.search',
|
|
53
66
|
route: '/api/goodvibes-agent/knowledge/search',
|
|
54
67
|
},
|
|
68
|
+
sourcesList: {
|
|
69
|
+
kind: 'agentKnowledge.sources.list',
|
|
70
|
+
route: '/api/goodvibes-agent/knowledge/sources',
|
|
71
|
+
},
|
|
72
|
+
nodesList: {
|
|
73
|
+
kind: 'agentKnowledge.nodes.list',
|
|
74
|
+
route: '/api/goodvibes-agent/knowledge/nodes',
|
|
75
|
+
},
|
|
76
|
+
issuesList: {
|
|
77
|
+
kind: 'agentKnowledge.issues.list',
|
|
78
|
+
route: '/api/goodvibes-agent/knowledge/issues',
|
|
79
|
+
},
|
|
80
|
+
itemGet: {
|
|
81
|
+
kind: 'agentKnowledge.item.get',
|
|
82
|
+
route: '/api/goodvibes-agent/knowledge/items/{id}',
|
|
83
|
+
},
|
|
84
|
+
map: {
|
|
85
|
+
kind: 'agentKnowledge.map',
|
|
86
|
+
route: '/api/goodvibes-agent/knowledge/map',
|
|
87
|
+
},
|
|
88
|
+
connectorsList: {
|
|
89
|
+
kind: 'agentKnowledge.connectors.list',
|
|
90
|
+
route: '/api/goodvibes-agent/knowledge/connectors',
|
|
91
|
+
},
|
|
55
92
|
ingestUrl: {
|
|
56
93
|
kind: 'agentKnowledge.ingest.url',
|
|
57
94
|
route: '/api/goodvibes-agent/knowledge/ingest/url',
|
|
58
95
|
},
|
|
96
|
+
ingestUrls: {
|
|
97
|
+
kind: 'agentKnowledge.ingest.urls',
|
|
98
|
+
route: '/api/goodvibes-agent/knowledge/ingest/urls',
|
|
99
|
+
},
|
|
100
|
+
ingestBookmarks: {
|
|
101
|
+
kind: 'agentKnowledge.ingest.bookmarks',
|
|
102
|
+
route: '/api/goodvibes-agent/knowledge/ingest/bookmarks',
|
|
103
|
+
},
|
|
104
|
+
reindex: {
|
|
105
|
+
kind: 'agentKnowledge.reindex',
|
|
106
|
+
route: '/api/goodvibes-agent/knowledge/reindex',
|
|
107
|
+
},
|
|
59
108
|
} as const;
|
|
60
109
|
|
|
61
110
|
const DELEGATION_METHOD = {
|
|
@@ -79,25 +128,6 @@ function readString(record: JsonRecord | null, key: string): string | null {
|
|
|
79
128
|
return typeof value === 'string' ? value : null;
|
|
80
129
|
}
|
|
81
130
|
|
|
82
|
-
function readNumber(record: JsonRecord | null, key: string): number | null {
|
|
83
|
-
const value = record?.[key];
|
|
84
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function readBoolean(record: JsonRecord | null, key: string): boolean | null {
|
|
88
|
-
const value = record?.[key];
|
|
89
|
-
return typeof value === 'boolean' ? value : null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function readArray(record: JsonRecord | null, key: string): readonly unknown[] {
|
|
93
|
-
const value = record?.[key];
|
|
94
|
-
return Array.isArray(value) ? value : [];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function cleanInline(value: unknown): string {
|
|
98
|
-
return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
99
|
-
}
|
|
100
|
-
|
|
101
131
|
function commandValues(args: readonly string[]): string[] {
|
|
102
132
|
const values: string[] = [];
|
|
103
133
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -265,6 +295,50 @@ async function postAgentKnowledgeJson<TData>(
|
|
|
265
295
|
return parsed as TData;
|
|
266
296
|
}
|
|
267
297
|
|
|
298
|
+
function queryRoute(route: string, query: JsonRecord): string {
|
|
299
|
+
const params = new URLSearchParams();
|
|
300
|
+
for (const [key, value] of Object.entries(query)) {
|
|
301
|
+
if (value === undefined || value === null || value === '') continue;
|
|
302
|
+
if (Array.isArray(value)) {
|
|
303
|
+
for (const item of value) {
|
|
304
|
+
if (typeof item === 'string' && item.trim().length > 0) params.append(key, item);
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
309
|
+
params.set(key, String(value));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const suffix = params.toString();
|
|
313
|
+
return suffix ? `${route}?${suffix}` : route;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function getAgentKnowledgeJson<TData>(
|
|
317
|
+
connection: AgentDaemonConnection,
|
|
318
|
+
route: string,
|
|
319
|
+
query: JsonRecord = {},
|
|
320
|
+
): Promise<TData> {
|
|
321
|
+
const response = await fetch(`${connection.baseUrl}${queryRoute(route, query)}`, {
|
|
322
|
+
headers: {
|
|
323
|
+
authorization: `Bearer ${connection.token ?? ''}`,
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
const text = await response.text();
|
|
327
|
+
let parsed: unknown = text;
|
|
328
|
+
if (text.trim()) {
|
|
329
|
+
try {
|
|
330
|
+
parsed = JSON.parse(text) as unknown;
|
|
331
|
+
} catch {
|
|
332
|
+
parsed = text;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!response.ok) {
|
|
336
|
+
const detail = isRecord(parsed) && typeof parsed.error === 'string' ? parsed.error : text;
|
|
337
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}${detail ? `: ${detail}` : ''}`);
|
|
338
|
+
}
|
|
339
|
+
return parsed as TData;
|
|
340
|
+
}
|
|
341
|
+
|
|
268
342
|
function findDisallowedKnowledgeScopeFlag(args: readonly string[]): string | null {
|
|
269
343
|
const disallowed = [
|
|
270
344
|
'--space',
|
|
@@ -292,111 +366,6 @@ function formatScopeFlagRejection(flag: string): string {
|
|
|
292
366
|
].join('\n');
|
|
293
367
|
}
|
|
294
368
|
|
|
295
|
-
function sourceLine(value: unknown): string {
|
|
296
|
-
const record = isRecord(value) ? value : {};
|
|
297
|
-
const title = cleanInline(record.title)
|
|
298
|
-
|| cleanInline(record.canonicalUri)
|
|
299
|
-
|| cleanInline(record.sourceUri)
|
|
300
|
-
|| cleanInline(record.url)
|
|
301
|
-
|| cleanInline(record.id)
|
|
302
|
-
|| 'untitled';
|
|
303
|
-
const type = cleanInline(record.sourceType) || cleanInline(record.type) || 'source';
|
|
304
|
-
const url = cleanInline(record.canonicalUri) || cleanInline(record.sourceUri) || cleanInline(record.url);
|
|
305
|
-
return url && url !== title ? `[${type}] ${title} (${url})` : `[${type}] ${title}`;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function resultLine(value: unknown): string {
|
|
309
|
-
const record = isRecord(value) ? value : {};
|
|
310
|
-
const title = cleanInline(record.title) || cleanInline(record.id) || 'untitled';
|
|
311
|
-
const id = cleanInline(record.id);
|
|
312
|
-
const type = cleanInline(record.type) || cleanInline(record.kind) || cleanInline(record.sourceType) || 'result';
|
|
313
|
-
const score = readNumber(record, 'score');
|
|
314
|
-
const url = cleanInline(record.url) || cleanInline(record.canonicalUri) || cleanInline(record.sourceUri);
|
|
315
|
-
const snippet = cleanInline(record.snippet) || cleanInline(record.summary) || cleanInline(record.text);
|
|
316
|
-
const parts = [
|
|
317
|
-
`[${type}] ${title}`,
|
|
318
|
-
id && id !== title ? `id=${id}` : '',
|
|
319
|
-
score !== null ? `score=${score.toFixed(3)}` : '',
|
|
320
|
-
url ? `url=${url}` : '',
|
|
321
|
-
].filter((part) => part.length > 0);
|
|
322
|
-
return snippet ? `${parts.join(' ')}\n ${snippet}` : parts.join(' ');
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function formatStatus(data: unknown): string {
|
|
326
|
-
const record = isRecord(data) ? data : {};
|
|
327
|
-
const ready = readBoolean(record, 'ready');
|
|
328
|
-
const sourceCount = readNumber(record, 'sourceCount');
|
|
329
|
-
const nodeCount = readNumber(record, 'nodeCount');
|
|
330
|
-
const issueCount = readNumber(record, 'issueCount');
|
|
331
|
-
const edgeCount = readNumber(record, 'edgeCount');
|
|
332
|
-
const storagePath = readString(record, 'storagePath');
|
|
333
|
-
return [
|
|
334
|
-
'Agent Knowledge status',
|
|
335
|
-
` ready: ${ready === null ? 'unknown' : yesNo(ready)}`,
|
|
336
|
-
` sources: ${sourceCount ?? 'unknown'}`,
|
|
337
|
-
` nodes: ${nodeCount ?? 'unknown'}`,
|
|
338
|
-
` edges: ${edgeCount ?? 'unknown'}`,
|
|
339
|
-
` issues: ${issueCount ?? 'unknown'}`,
|
|
340
|
-
storagePath ? ` storage: ${storagePath}` : null,
|
|
341
|
-
' route: /api/goodvibes-agent/knowledge/status',
|
|
342
|
-
].filter((line): line is string => Boolean(line)).join('\n');
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function formatAsk(data: unknown, query: string): string {
|
|
346
|
-
const record = isRecord(data) ? data : {};
|
|
347
|
-
const answer = isRecord(record.answer) ? record.answer : record;
|
|
348
|
-
const text = cleanInline(answer.text) || cleanInline(record.answer) || 'No answer returned.';
|
|
349
|
-
const confidence = readNumber(answer, 'confidence') ?? readNumber(record, 'confidence');
|
|
350
|
-
const synthesized = readBoolean(answer, 'synthesized');
|
|
351
|
-
const sources = readArray(answer, 'sources');
|
|
352
|
-
const facts = readArray(answer, 'facts');
|
|
353
|
-
const gaps = readArray(answer, 'gaps');
|
|
354
|
-
const lines = [
|
|
355
|
-
`Agent Knowledge answer: ${query}`,
|
|
356
|
-
text,
|
|
357
|
-
'',
|
|
358
|
-
`confidence: ${confidence ?? 'unknown'}${synthesized === null ? '' : ` synthesized: ${yesNo(synthesized)}`}`,
|
|
359
|
-
];
|
|
360
|
-
if (sources.length > 0) {
|
|
361
|
-
lines.push('', 'Sources:', ...sources.slice(0, 8).map((source) => ` - ${sourceLine(source)}`));
|
|
362
|
-
}
|
|
363
|
-
if (facts.length > 0) lines.push('', `Facts: ${facts.length}`);
|
|
364
|
-
if (gaps.length > 0) lines.push('', `Gaps: ${gaps.length}`);
|
|
365
|
-
return lines.join('\n');
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function formatSearch(data: unknown, query: string): string {
|
|
369
|
-
const record = isRecord(data) ? data : {};
|
|
370
|
-
const items = readArray(record, 'items');
|
|
371
|
-
const results = items.length > 0 ? items : readArray(record, 'results');
|
|
372
|
-
if (results.length === 0) {
|
|
373
|
-
return [
|
|
374
|
-
`Agent Knowledge search: ${query}`,
|
|
375
|
-
' no results',
|
|
376
|
-
' route: /api/goodvibes-agent/knowledge/search',
|
|
377
|
-
].join('\n');
|
|
378
|
-
}
|
|
379
|
-
return [
|
|
380
|
-
`Agent Knowledge search: ${query}`,
|
|
381
|
-
...results.slice(0, 10).map((result, index) => ` ${index + 1}. ${resultLine(result)}`),
|
|
382
|
-
].join('\n');
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function formatIngest(data: unknown, url: string): string {
|
|
386
|
-
const record = isRecord(data) ? data : {};
|
|
387
|
-
const source = isRecord(record.source) ? record.source : record;
|
|
388
|
-
const sourceId = cleanInline(source.id);
|
|
389
|
-
const canonicalUri = cleanInline(source.canonicalUri) || cleanInline(source.sourceUri) || url;
|
|
390
|
-
const artifactId = cleanInline(record.artifactId);
|
|
391
|
-
return [
|
|
392
|
-
'Agent Knowledge ingest-url accepted',
|
|
393
|
-
` source: ${sourceId || '(pending)'}`,
|
|
394
|
-
` url: ${canonicalUri}`,
|
|
395
|
-
artifactId ? ` artifact: ${artifactId}` : null,
|
|
396
|
-
' route: /api/goodvibes-agent/knowledge/ingest/url',
|
|
397
|
-
].filter((line): line is string => Boolean(line)).join('\n');
|
|
398
|
-
}
|
|
399
|
-
|
|
400
369
|
function buildDelegationBody(task: string, wrfcRequested: boolean): string {
|
|
401
370
|
return [
|
|
402
371
|
'GoodVibes Agent explicit build delegation.',
|
|
@@ -414,25 +383,6 @@ function buildDelegationBody(task: string, wrfcRequested: boolean): string {
|
|
|
414
383
|
].join('\n');
|
|
415
384
|
}
|
|
416
385
|
|
|
417
|
-
function formatFailure(failure: AgentKnowledgeFailure, json: boolean): string {
|
|
418
|
-
if (json) return JSON.stringify(failure, null, 2);
|
|
419
|
-
return [
|
|
420
|
-
`Agent Knowledge error: ${failure.kind}`,
|
|
421
|
-
` ${failure.error}`,
|
|
422
|
-
` runtime: ${failure.baseUrl}`,
|
|
423
|
-
` route: ${failure.route}`,
|
|
424
|
-
failure.kind === 'version_mismatch' && failure.daemonVersion && failure.expectedSdkVersion
|
|
425
|
-
? ` versions: runtime=${failure.daemonVersion} expected=${failure.expectedSdkVersion}`
|
|
426
|
-
: null,
|
|
427
|
-
failure.kind === 'version_mismatch'
|
|
428
|
-
? ' next: update/restart the external GoodVibes runtime so /status matches the Agent SDK pin.'
|
|
429
|
-
: null,
|
|
430
|
-
failure.kind === 'daemon_route_unavailable'
|
|
431
|
-
? ' next: update/restart the external GoodVibes runtime to the SDK version required by this Agent package.'
|
|
432
|
-
: null,
|
|
433
|
-
].filter((line): line is string => Boolean(line)).join('\n');
|
|
434
|
-
}
|
|
435
|
-
|
|
436
386
|
async function runKnowledgeCall<TData>(
|
|
437
387
|
runtime: CliCommandRuntime,
|
|
438
388
|
method: DaemonCallMethod,
|
|
@@ -524,6 +474,63 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
|
|
|
524
474
|
};
|
|
525
475
|
}
|
|
526
476
|
|
|
477
|
+
if (normalized === 'list' || normalized === 'sources' || normalized === 'nodes' || normalized === 'issues') {
|
|
478
|
+
const requestedKind = normalized === 'list' ? (readOptionValue(rest, '--kind') ?? 'sources').toLowerCase() : normalized;
|
|
479
|
+
const kind = requestedKind === 'nodes' || requestedKind === 'issues' ? requestedKind : 'sources';
|
|
480
|
+
const limit = readPositiveInt(rest, '--limit', 10);
|
|
481
|
+
const method = kind === 'sources'
|
|
482
|
+
? AGENT_KNOWLEDGE_METHODS.sourcesList
|
|
483
|
+
: kind === 'nodes'
|
|
484
|
+
? AGENT_KNOWLEDGE_METHODS.nodesList
|
|
485
|
+
: AGENT_KNOWLEDGE_METHODS.issuesList;
|
|
486
|
+
const result = await runKnowledgeCall(runtime, method, async (connection) => (
|
|
487
|
+
await getAgentKnowledgeJson(connection, method.route, { limit })
|
|
488
|
+
));
|
|
489
|
+
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
490
|
+
return {
|
|
491
|
+
output: formatJsonOrText(runtime.cli)(result, formatEntityList(result.data, kind, limit)),
|
|
492
|
+
exitCode: 0,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (normalized === 'get') {
|
|
497
|
+
const [id] = commandValues(rest);
|
|
498
|
+
if (!id) return { output: 'Usage: goodvibes-agent knowledge get <source|node|issue id>', exitCode: 2 };
|
|
499
|
+
const route = `/api/goodvibes-agent/knowledge/items/${encodeURIComponent(id)}`;
|
|
500
|
+
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.itemGet, async (connection) => (
|
|
501
|
+
await getAgentKnowledgeJson(connection, route)
|
|
502
|
+
));
|
|
503
|
+
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
504
|
+
return {
|
|
505
|
+
output: formatJsonOrText(runtime.cli)(result, formatItem(result.data, id)),
|
|
506
|
+
exitCode: 0,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (normalized === 'map') {
|
|
511
|
+
const limit = readPositiveInt(rest, '--limit', 50);
|
|
512
|
+
const query = commandValues(rest).join(' ').trim();
|
|
513
|
+
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.map, async (connection) => (
|
|
514
|
+
await getAgentKnowledgeJson(connection, AGENT_KNOWLEDGE_METHODS.map.route, { limit, query })
|
|
515
|
+
));
|
|
516
|
+
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
517
|
+
return {
|
|
518
|
+
output: formatJsonOrText(runtime.cli)(result, formatMap(result.data)),
|
|
519
|
+
exitCode: 0,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (normalized === 'connectors') {
|
|
524
|
+
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.connectorsList, async (connection) => (
|
|
525
|
+
await getAgentKnowledgeJson(connection, AGENT_KNOWLEDGE_METHODS.connectorsList.route)
|
|
526
|
+
));
|
|
527
|
+
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
528
|
+
return {
|
|
529
|
+
output: formatJsonOrText(runtime.cli)(result, formatConnectors(result.data)),
|
|
530
|
+
exitCode: 0,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
527
534
|
if (normalized === 'ingest-url') {
|
|
528
535
|
const values = commandValues(rest);
|
|
529
536
|
const url = values[0];
|
|
@@ -558,8 +565,69 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
|
|
|
558
565
|
};
|
|
559
566
|
}
|
|
560
567
|
|
|
568
|
+
if (normalized === 'import-urls' || normalized === 'import-bookmarks') {
|
|
569
|
+
const values = commandValues(rest);
|
|
570
|
+
const path = values[0];
|
|
571
|
+
const method = normalized === 'import-urls'
|
|
572
|
+
? AGENT_KNOWLEDGE_METHODS.ingestUrls
|
|
573
|
+
: AGENT_KNOWLEDGE_METHODS.ingestBookmarks;
|
|
574
|
+
if (!path) {
|
|
575
|
+
return {
|
|
576
|
+
output: `Usage: goodvibes-agent knowledge ${normalized} <path> [--allow-private-hosts] --yes`,
|
|
577
|
+
exitCode: 2,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (!confirmation.present) {
|
|
581
|
+
const failure = {
|
|
582
|
+
ok: false,
|
|
583
|
+
kind: 'confirmation_required',
|
|
584
|
+
error: `Refusing to import ${path} into Agent Knowledge without --yes.`,
|
|
585
|
+
route: method.route,
|
|
586
|
+
};
|
|
587
|
+
return {
|
|
588
|
+
output: json ? JSON.stringify(failure, null, 2) : `${failure.error}\nUsage: goodvibes-agent knowledge ${normalized} <path> [--allow-private-hosts] --yes`,
|
|
589
|
+
exitCode: 2,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
const result = await runKnowledgeCall(runtime, method, async (connection) => (
|
|
593
|
+
await postAgentKnowledgeJson(connection, method.route, {
|
|
594
|
+
path,
|
|
595
|
+
allowPrivateHosts: hasFlag(rest, '--allow-private-hosts'),
|
|
596
|
+
metadata: { originSurface: 'goodvibes-agent-cli' },
|
|
597
|
+
})
|
|
598
|
+
));
|
|
599
|
+
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
600
|
+
return {
|
|
601
|
+
output: formatJsonOrText(runtime.cli)(result, formatBatchIngest(result.data, normalized)),
|
|
602
|
+
exitCode: 0,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (normalized === 'reindex') {
|
|
607
|
+
if (!confirmation.present) {
|
|
608
|
+
const failure = {
|
|
609
|
+
ok: false,
|
|
610
|
+
kind: 'confirmation_required',
|
|
611
|
+
error: 'Refusing to reindex Agent Knowledge without --yes.',
|
|
612
|
+
route: AGENT_KNOWLEDGE_METHODS.reindex.route,
|
|
613
|
+
};
|
|
614
|
+
return {
|
|
615
|
+
output: json ? JSON.stringify(failure, null, 2) : `${failure.error}\nUsage: goodvibes-agent knowledge reindex --yes`,
|
|
616
|
+
exitCode: 2,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.reindex, async (connection) => (
|
|
620
|
+
await postAgentKnowledgeJson(connection, AGENT_KNOWLEDGE_METHODS.reindex.route, {})
|
|
621
|
+
));
|
|
622
|
+
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
623
|
+
return {
|
|
624
|
+
output: formatJsonOrText(runtime.cli)(result, formatReindex(result.data)),
|
|
625
|
+
exitCode: 0,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
561
629
|
return {
|
|
562
|
-
output: 'Usage: goodvibes-agent knowledge [status|ask <question>|search <query>|ingest-url <url> --yes]',
|
|
630
|
+
output: 'Usage: goodvibes-agent knowledge [status|ask <question>|search <query>|list|sources|nodes|issues|get <id>|map|connectors|ingest-url <url> --yes|import-urls <path> --yes|import-bookmarks <path> --yes|reindex --yes]',
|
|
563
631
|
exitCode: 2,
|
|
564
632
|
};
|
|
565
633
|
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
type JsonRecord = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
export interface AgentKnowledgeFailureLike {
|
|
4
|
+
readonly ok: false;
|
|
5
|
+
readonly kind: string;
|
|
6
|
+
readonly error: string;
|
|
7
|
+
readonly baseUrl: string;
|
|
8
|
+
readonly route: string;
|
|
9
|
+
readonly daemonVersion?: string;
|
|
10
|
+
readonly expectedSdkVersion?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isRecord(value: unknown): value is JsonRecord {
|
|
14
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readString(record: JsonRecord | null, key: string): string | null {
|
|
18
|
+
const value = record?.[key];
|
|
19
|
+
return typeof value === 'string' ? value : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readNumber(record: JsonRecord | null, key: string): number | null {
|
|
23
|
+
const value = record?.[key];
|
|
24
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readBoolean(record: JsonRecord | null, key: string): boolean | null {
|
|
28
|
+
const value = record?.[key];
|
|
29
|
+
return typeof value === 'boolean' ? value : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readArray(record: JsonRecord | null, key: string): readonly unknown[] {
|
|
33
|
+
const value = record?.[key];
|
|
34
|
+
return Array.isArray(value) ? value : [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cleanInline(value: unknown): string {
|
|
38
|
+
return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function yesNo(value: boolean): string {
|
|
42
|
+
return value ? 'yes' : 'no';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sourceLine(value: unknown): string {
|
|
46
|
+
const record = isRecord(value) ? value : {};
|
|
47
|
+
const title = cleanInline(record.title)
|
|
48
|
+
|| cleanInline(record.canonicalUri)
|
|
49
|
+
|| cleanInline(record.sourceUri)
|
|
50
|
+
|| cleanInline(record.url)
|
|
51
|
+
|| cleanInline(record.id)
|
|
52
|
+
|| 'untitled';
|
|
53
|
+
const type = cleanInline(record.sourceType) || cleanInline(record.type) || 'source';
|
|
54
|
+
const url = cleanInline(record.canonicalUri) || cleanInline(record.sourceUri) || cleanInline(record.url);
|
|
55
|
+
return url && url !== title ? `[${type}] ${title} (${url})` : `[${type}] ${title}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resultLine(value: unknown): string {
|
|
59
|
+
const record = isRecord(value) ? value : {};
|
|
60
|
+
const title = cleanInline(record.title) || cleanInline(record.id) || 'untitled';
|
|
61
|
+
const id = cleanInline(record.id);
|
|
62
|
+
const type = cleanInline(record.type) || cleanInline(record.kind) || cleanInline(record.sourceType) || 'result';
|
|
63
|
+
const score = readNumber(record, 'score');
|
|
64
|
+
const url = cleanInline(record.url) || cleanInline(record.canonicalUri) || cleanInline(record.sourceUri);
|
|
65
|
+
const snippet = cleanInline(record.snippet) || cleanInline(record.summary) || cleanInline(record.text);
|
|
66
|
+
const parts = [
|
|
67
|
+
`[${type}] ${title}`,
|
|
68
|
+
id && id !== title ? `id=${id}` : '',
|
|
69
|
+
score !== null ? `score=${score.toFixed(3)}` : '',
|
|
70
|
+
url ? `url=${url}` : '',
|
|
71
|
+
].filter((part) => part.length > 0);
|
|
72
|
+
return snippet ? `${parts.join(' ')}\n ${snippet}` : parts.join(' ');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function nodeLine(value: unknown): string {
|
|
76
|
+
const record = isRecord(value) ? value : {};
|
|
77
|
+
const id = cleanInline(record.id);
|
|
78
|
+
const kind = cleanInline(record.kind) || 'node';
|
|
79
|
+
const title = cleanInline(record.title) || id || 'untitled';
|
|
80
|
+
const confidence = readNumber(record, 'confidence');
|
|
81
|
+
const status = cleanInline(record.status);
|
|
82
|
+
const summary = cleanInline(record.summary);
|
|
83
|
+
const parts = [
|
|
84
|
+
`[${kind}] ${title}`,
|
|
85
|
+
id && id !== title ? `id=${id}` : '',
|
|
86
|
+
status ? `status=${status}` : '',
|
|
87
|
+
confidence !== null ? `confidence=${confidence}` : '',
|
|
88
|
+
].filter((part) => part.length > 0);
|
|
89
|
+
return summary ? `${parts.join(' ')}\n ${summary}` : parts.join(' ');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function issueLine(value: unknown): string {
|
|
93
|
+
const record = isRecord(value) ? value : {};
|
|
94
|
+
const id = cleanInline(record.id) || 'issue';
|
|
95
|
+
const severity = cleanInline(record.severity) || 'unknown';
|
|
96
|
+
const code = cleanInline(record.code) || 'issue';
|
|
97
|
+
const status = cleanInline(record.status);
|
|
98
|
+
const message = cleanInline(record.message);
|
|
99
|
+
const suffix = status ? ` status=${status}` : '';
|
|
100
|
+
return ` - ${id} [${severity}] ${code}${suffix}${message ? ` - ${message}` : ''}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function connectorLine(value: unknown): string {
|
|
104
|
+
const record = isRecord(value) ? value : {};
|
|
105
|
+
const id = cleanInline(record.id) || 'connector';
|
|
106
|
+
const name = cleanInline(record.displayName) || id;
|
|
107
|
+
const sourceType = cleanInline(record.sourceType);
|
|
108
|
+
const description = cleanInline(record.description);
|
|
109
|
+
const suffix = sourceType ? ` sourceType=${sourceType}` : '';
|
|
110
|
+
return description ? ` - ${id} ${name}${suffix}\n ${description}` : ` - ${id} ${name}${suffix}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatStatus(data: unknown): string {
|
|
114
|
+
const record = isRecord(data) ? data : {};
|
|
115
|
+
const ready = readBoolean(record, 'ready');
|
|
116
|
+
const sourceCount = readNumber(record, 'sourceCount');
|
|
117
|
+
const nodeCount = readNumber(record, 'nodeCount');
|
|
118
|
+
const issueCount = readNumber(record, 'issueCount');
|
|
119
|
+
const edgeCount = readNumber(record, 'edgeCount');
|
|
120
|
+
const storagePath = readString(record, 'storagePath');
|
|
121
|
+
return [
|
|
122
|
+
'Agent Knowledge status',
|
|
123
|
+
` ready: ${ready === null ? 'unknown' : yesNo(ready)}`,
|
|
124
|
+
` sources: ${sourceCount ?? 'unknown'}`,
|
|
125
|
+
` nodes: ${nodeCount ?? 'unknown'}`,
|
|
126
|
+
` edges: ${edgeCount ?? 'unknown'}`,
|
|
127
|
+
` issues: ${issueCount ?? 'unknown'}`,
|
|
128
|
+
storagePath ? ` storage: ${storagePath}` : null,
|
|
129
|
+
' route: /api/goodvibes-agent/knowledge/status',
|
|
130
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function formatEntityList(data: unknown, kind: 'sources' | 'nodes' | 'issues', limit: number): string {
|
|
134
|
+
const record = isRecord(data) ? data : {};
|
|
135
|
+
const values = readArray(record, kind);
|
|
136
|
+
if (values.length === 0) {
|
|
137
|
+
return [
|
|
138
|
+
`Agent Knowledge ${kind}`,
|
|
139
|
+
' no records',
|
|
140
|
+
` route: /api/goodvibes-agent/knowledge/${kind}`,
|
|
141
|
+
].join('\n');
|
|
142
|
+
}
|
|
143
|
+
const format = kind === 'sources'
|
|
144
|
+
? sourceLine
|
|
145
|
+
: kind === 'nodes'
|
|
146
|
+
? nodeLine
|
|
147
|
+
: issueLine;
|
|
148
|
+
return [
|
|
149
|
+
`Agent Knowledge ${kind} (${values.length}, limit ${limit})`,
|
|
150
|
+
...values.slice(0, limit).map((value, index) => (
|
|
151
|
+
kind === 'issues' ? format(value) : ` ${index + 1}. ${format(value)}`
|
|
152
|
+
)),
|
|
153
|
+
].join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatItem(data: unknown, id: string): string {
|
|
157
|
+
const record = isRecord(data) ? data : {};
|
|
158
|
+
const source = record.source;
|
|
159
|
+
const node = record.node;
|
|
160
|
+
const issue = record.issue;
|
|
161
|
+
const relatedEdges = readArray(record, 'relatedEdges').length;
|
|
162
|
+
const linkedSources = readArray(record, 'linkedSources').length;
|
|
163
|
+
const linkedNodes = readArray(record, 'linkedNodes').length;
|
|
164
|
+
if (source) {
|
|
165
|
+
return [
|
|
166
|
+
`Agent Knowledge item: ${id}`,
|
|
167
|
+
` ${sourceLine(source)}`,
|
|
168
|
+
` relatedEdges=${relatedEdges} linkedSources=${linkedSources} linkedNodes=${linkedNodes}`,
|
|
169
|
+
].join('\n');
|
|
170
|
+
}
|
|
171
|
+
if (node) {
|
|
172
|
+
return [
|
|
173
|
+
`Agent Knowledge item: ${id}`,
|
|
174
|
+
` ${nodeLine(node)}`,
|
|
175
|
+
` relatedEdges=${relatedEdges} linkedSources=${linkedSources} linkedNodes=${linkedNodes}`,
|
|
176
|
+
].join('\n');
|
|
177
|
+
}
|
|
178
|
+
if (issue) {
|
|
179
|
+
return [
|
|
180
|
+
`Agent Knowledge item: ${id}`,
|
|
181
|
+
issueLine(issue),
|
|
182
|
+
` relatedEdges=${relatedEdges} linkedSources=${linkedSources} linkedNodes=${linkedNodes}`,
|
|
183
|
+
].join('\n');
|
|
184
|
+
}
|
|
185
|
+
return [
|
|
186
|
+
`Agent Knowledge item: ${id}`,
|
|
187
|
+
' not found',
|
|
188
|
+
' route: /api/goodvibes-agent/knowledge/items/{id}',
|
|
189
|
+
].join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function formatMap(data: unknown): string {
|
|
193
|
+
const record = isRecord(data) ? data : {};
|
|
194
|
+
const sources = readArray(record, 'sources');
|
|
195
|
+
const nodes = readArray(record, 'nodes');
|
|
196
|
+
const edges = readArray(record, 'edges');
|
|
197
|
+
const issues = readArray(record, 'issues');
|
|
198
|
+
return [
|
|
199
|
+
'Agent Knowledge map',
|
|
200
|
+
` sources: ${sources.length}`,
|
|
201
|
+
` nodes: ${nodes.length}`,
|
|
202
|
+
` edges: ${edges.length}`,
|
|
203
|
+
` issues: ${issues.length}`,
|
|
204
|
+
' route: /api/goodvibes-agent/knowledge/map',
|
|
205
|
+
].join('\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function formatConnectors(data: unknown): string {
|
|
209
|
+
const record = isRecord(data) ? data : {};
|
|
210
|
+
const connectors = readArray(record, 'connectors');
|
|
211
|
+
if (connectors.length === 0) {
|
|
212
|
+
return [
|
|
213
|
+
'Agent Knowledge connectors',
|
|
214
|
+
' no connectors',
|
|
215
|
+
' route: /api/goodvibes-agent/knowledge/connectors',
|
|
216
|
+
].join('\n');
|
|
217
|
+
}
|
|
218
|
+
return [
|
|
219
|
+
`Agent Knowledge connectors (${connectors.length})`,
|
|
220
|
+
...connectors.map(connectorLine),
|
|
221
|
+
].join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function formatAsk(data: unknown, query: string): string {
|
|
225
|
+
const record = isRecord(data) ? data : {};
|
|
226
|
+
const answer = isRecord(record.answer) ? record.answer : record;
|
|
227
|
+
const text = cleanInline(answer.text) || cleanInline(record.answer) || 'No answer returned.';
|
|
228
|
+
const confidence = readNumber(answer, 'confidence') ?? readNumber(record, 'confidence');
|
|
229
|
+
const synthesized = readBoolean(answer, 'synthesized');
|
|
230
|
+
const sources = readArray(answer, 'sources');
|
|
231
|
+
const facts = readArray(answer, 'facts');
|
|
232
|
+
const gaps = readArray(answer, 'gaps');
|
|
233
|
+
const lines = [
|
|
234
|
+
`Agent Knowledge answer: ${query}`,
|
|
235
|
+
text,
|
|
236
|
+
'',
|
|
237
|
+
`confidence: ${confidence ?? 'unknown'}${synthesized === null ? '' : ` synthesized: ${yesNo(synthesized)}`}`,
|
|
238
|
+
];
|
|
239
|
+
if (sources.length > 0) {
|
|
240
|
+
lines.push('', 'Sources:', ...sources.slice(0, 8).map((source) => ` - ${sourceLine(source)}`));
|
|
241
|
+
}
|
|
242
|
+
if (facts.length > 0) lines.push('', `Facts: ${facts.length}`);
|
|
243
|
+
if (gaps.length > 0) lines.push('', `Gaps: ${gaps.length}`);
|
|
244
|
+
return lines.join('\n');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function formatSearch(data: unknown, query: string): string {
|
|
248
|
+
const record = isRecord(data) ? data : {};
|
|
249
|
+
const items = readArray(record, 'items');
|
|
250
|
+
const results = items.length > 0 ? items : readArray(record, 'results');
|
|
251
|
+
if (results.length === 0) {
|
|
252
|
+
return [
|
|
253
|
+
`Agent Knowledge search: ${query}`,
|
|
254
|
+
' no results',
|
|
255
|
+
' route: /api/goodvibes-agent/knowledge/search',
|
|
256
|
+
].join('\n');
|
|
257
|
+
}
|
|
258
|
+
return [
|
|
259
|
+
`Agent Knowledge search: ${query}`,
|
|
260
|
+
...results.slice(0, 10).map((result, index) => ` ${index + 1}. ${resultLine(result)}`),
|
|
261
|
+
].join('\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function formatIngest(data: unknown, url: string): string {
|
|
265
|
+
const record = isRecord(data) ? data : {};
|
|
266
|
+
const source = isRecord(record.source) ? record.source : record;
|
|
267
|
+
const sourceId = cleanInline(source.id);
|
|
268
|
+
const canonicalUri = cleanInline(source.canonicalUri) || cleanInline(source.sourceUri) || url;
|
|
269
|
+
const artifactId = cleanInline(record.artifactId);
|
|
270
|
+
return [
|
|
271
|
+
'Agent Knowledge ingest-url accepted',
|
|
272
|
+
` source: ${sourceId || '(pending)'}`,
|
|
273
|
+
` url: ${canonicalUri}`,
|
|
274
|
+
artifactId ? ` artifact: ${artifactId}` : null,
|
|
275
|
+
' route: /api/goodvibes-agent/knowledge/ingest/url',
|
|
276
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function formatBatchIngest(data: unknown, label: string): string {
|
|
280
|
+
const record = isRecord(data) ? data : {};
|
|
281
|
+
const imported = readNumber(record, 'imported');
|
|
282
|
+
const failed = readNumber(record, 'failed');
|
|
283
|
+
const sources = readArray(record, 'sources');
|
|
284
|
+
const errors = readArray(record, 'errors');
|
|
285
|
+
return [
|
|
286
|
+
`Agent Knowledge ${label} accepted`,
|
|
287
|
+
` imported: ${imported ?? sources.length}`,
|
|
288
|
+
` failed: ${failed ?? errors.length}`,
|
|
289
|
+
` sources: ${sources.length}`,
|
|
290
|
+
...sources.slice(0, 5).map((source) => ` - ${sourceLine(source)}`),
|
|
291
|
+
...(errors.length > 0 ? [' errors:', ...errors.slice(0, 5).map((error) => ` - ${cleanInline(error)}`)] : []),
|
|
292
|
+
].join('\n');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function formatReindex(data: unknown): string {
|
|
296
|
+
const record = isRecord(data) ? data : {};
|
|
297
|
+
const status = isRecord(record.status) ? record.status : {};
|
|
298
|
+
return [
|
|
299
|
+
'Agent Knowledge reindex complete',
|
|
300
|
+
` sources: ${readNumber(status, 'sourceCount') ?? 'unknown'}`,
|
|
301
|
+
` nodes: ${readNumber(status, 'nodeCount') ?? 'unknown'}`,
|
|
302
|
+
` edges: ${readNumber(status, 'edgeCount') ?? 'unknown'}`,
|
|
303
|
+
` issues: ${readNumber(status, 'issueCount') ?? 'unknown'}`,
|
|
304
|
+
' route: /api/goodvibes-agent/knowledge/reindex',
|
|
305
|
+
].join('\n');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function formatFailure(failure: AgentKnowledgeFailureLike, json: boolean): string {
|
|
309
|
+
if (json) return JSON.stringify(failure, null, 2);
|
|
310
|
+
return [
|
|
311
|
+
`Agent Knowledge error: ${failure.kind}`,
|
|
312
|
+
` ${failure.error}`,
|
|
313
|
+
` runtime: ${failure.baseUrl}`,
|
|
314
|
+
` route: ${failure.route}`,
|
|
315
|
+
failure.kind === 'version_mismatch' && failure.daemonVersion && failure.expectedSdkVersion
|
|
316
|
+
? ` versions: runtime=${failure.daemonVersion} expected=${failure.expectedSdkVersion}`
|
|
317
|
+
: null,
|
|
318
|
+
failure.kind === 'version_mismatch'
|
|
319
|
+
? ' next: update/restart the external GoodVibes runtime so /status matches the Agent SDK pin.'
|
|
320
|
+
: null,
|
|
321
|
+
failure.kind === 'daemon_route_unavailable'
|
|
322
|
+
? ' next: update/restart the external GoodVibes runtime to the SDK version required by this Agent package.'
|
|
323
|
+
: null,
|
|
324
|
+
].filter((line): line is string => Boolean(line)).join('\n');
|
|
325
|
+
}
|
|
@@ -125,7 +125,7 @@ export async function handleBundleCommand(runtime: CliCommandRuntime): Promise<C
|
|
|
125
125
|
|
|
126
126
|
if (sub === 'inspect') {
|
|
127
127
|
const path = rest[0];
|
|
128
|
-
if (!path) return { output:
|
|
128
|
+
if (!path) return { output: `Usage: ${runtime.cli.binary} bundle inspect <path>`, exitCode: 2 };
|
|
129
129
|
const sourcePath = shellPaths.resolveWorkspacePath(path);
|
|
130
130
|
const parsed = readJsonFile(sourcePath);
|
|
131
131
|
if (!parsed.ok) return { output: `Invalid bundle JSON: ${parsed.error}`, exitCode: 1 };
|
|
@@ -199,7 +199,7 @@ export async function handleBundleCommand(runtime: CliCommandRuntime): Promise<C
|
|
|
199
199
|
|
|
200
200
|
if (sub === 'import') {
|
|
201
201
|
const path = rest[0];
|
|
202
|
-
if (!path) return { output:
|
|
202
|
+
if (!path) return { output: `Usage: ${runtime.cli.binary} bundle import <path>`, exitCode: 2 };
|
|
203
203
|
const sourcePath = shellPaths.resolveWorkspacePath(path);
|
|
204
204
|
const parsed = readJsonFile(sourcePath);
|
|
205
205
|
if (!parsed.ok) return { output: `Invalid bundle JSON: ${parsed.error}`, exitCode: 1 };
|
|
@@ -223,5 +223,5 @@ export async function handleBundleCommand(runtime: CliCommandRuntime): Promise<C
|
|
|
223
223
|
};
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
return { output:
|
|
226
|
+
return { output: `Usage: ${runtime.cli.binary} bundle export [path]|inspect <path>|import <path>`, exitCode: 2 };
|
|
227
227
|
}
|
package/src/cli/help.ts
CHANGED
|
@@ -184,14 +184,23 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
184
184
|
'knowledge status',
|
|
185
185
|
'knowledge ask <question> [--limit <n>] [--mode concise|standard|detailed]',
|
|
186
186
|
'knowledge search <query> [--limit <n>]',
|
|
187
|
+
'knowledge list [--kind sources|nodes|issues] [--limit <n>]',
|
|
188
|
+
'knowledge get <id>',
|
|
189
|
+
'knowledge connectors',
|
|
187
190
|
'knowledge ingest-url <url> [--title <title>] [--tags a,b] --yes',
|
|
191
|
+
'knowledge import-urls <path> --yes',
|
|
192
|
+
'knowledge import-bookmarks <path> --yes',
|
|
193
|
+
'knowledge reindex --yes',
|
|
188
194
|
],
|
|
189
195
|
summary: 'Call isolated Agent Knowledge/Wiki routes under /api/goodvibes-agent/knowledge. No default wiki or non-Agent fallback.',
|
|
190
196
|
examples: [
|
|
191
197
|
'knowledge status',
|
|
192
198
|
'knowledge ask "What is GoodVibes Agent?"',
|
|
193
199
|
'knowledge search "release checklist"',
|
|
200
|
+
'knowledge list --kind sources',
|
|
201
|
+
'knowledge connectors',
|
|
194
202
|
'knowledge ingest-url https://example.com/page --title "Reference" --yes',
|
|
203
|
+
'knowledge import-urls ~/agent-links.txt --yes',
|
|
195
204
|
],
|
|
196
205
|
},
|
|
197
206
|
ask: {
|
|
@@ -213,7 +222,7 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
213
222
|
],
|
|
214
223
|
},
|
|
215
224
|
subscription: {
|
|
216
|
-
usage: ['subscription list', 'subscription providers', 'subscription inspect <provider>', 'subscription login <provider> start
|
|
225
|
+
usage: ['subscription list', 'subscription providers', 'subscription inspect <provider>', 'subscription login <provider> start [--open]', 'subscription login <provider> finish <code-or-url>', 'subscription logout <provider>'],
|
|
217
226
|
summary: 'Manage OAuth/subscription-backed provider sessions such as OpenAI subscription access.',
|
|
218
227
|
examples: ['subscription providers', 'subscription login openai start --open', 'subscription inspect openai'],
|
|
219
228
|
},
|
|
@@ -14,6 +14,7 @@ import { GOODVIBES_AGENT_PAIRING_SURFACE } from '../config/surface.ts';
|
|
|
14
14
|
|
|
15
15
|
export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<string> {
|
|
16
16
|
return await withRuntimeServices(runtime, async (services) => {
|
|
17
|
+
const binary = runtime.cli.binary;
|
|
17
18
|
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
18
19
|
const subscriptions = services.subscriptionManager.list();
|
|
19
20
|
const pending = services.subscriptionManager.listPending();
|
|
@@ -26,7 +27,7 @@ export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<s
|
|
|
26
27
|
}
|
|
27
28
|
if (sub === 'inspect' || sub === 'show') {
|
|
28
29
|
const provider = rest[0];
|
|
29
|
-
if (!provider) return
|
|
30
|
+
if (!provider) return `Usage: ${binary} subscription inspect <provider>`;
|
|
30
31
|
const resolved = getSubscriptionProviderConfig(provider, services.serviceRegistry.get(provider));
|
|
31
32
|
if (!resolved && !services.subscriptionManager.get(provider) && !services.subscriptionManager.getPending(provider)) {
|
|
32
33
|
return `No stored or available subscription provider named ${provider}.`;
|
|
@@ -59,7 +60,7 @@ export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<s
|
|
|
59
60
|
if (sub === 'login' || sub === 'start') {
|
|
60
61
|
const provider = sub === 'start' ? rest[0] : rest[0];
|
|
61
62
|
const mode = sub === 'start' ? 'start' : rest[1]?.toLowerCase();
|
|
62
|
-
if (!provider || mode !== 'start') return
|
|
63
|
+
if (!provider || mode !== 'start') return `Usage: ${binary} subscription login <provider> start [--open]`;
|
|
63
64
|
const resolved = getSubscriptionProviderConfig(provider, services.serviceRegistry.get(provider));
|
|
64
65
|
if (!resolved) return `No subscription provider found: ${provider}`;
|
|
65
66
|
if (provider === 'openai' && resolved.source === 'builtin') {
|
|
@@ -78,7 +79,7 @@ export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<s
|
|
|
78
79
|
` state: ${started.state}`,
|
|
79
80
|
` redirectUri: ${started.redirectUri}`,
|
|
80
81
|
...(openResult ? [` open: ${openResult}`] : []),
|
|
81
|
-
` next:
|
|
82
|
+
` next: ${binary} subscription login ${provider} finish <code-or-url>`,
|
|
82
83
|
' authorizationUrl:',
|
|
83
84
|
` ${started.authorizationUrl}`,
|
|
84
85
|
].join('\n');
|
|
@@ -91,7 +92,7 @@ export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<s
|
|
|
91
92
|
` state: ${started.pending.state}`,
|
|
92
93
|
` redirectUri: ${started.pending.redirectUri}`,
|
|
93
94
|
...(openResult ? [` open: ${openResult}`] : []),
|
|
94
|
-
` next:
|
|
95
|
+
` next: ${binary} subscription login ${provider} finish <code-or-url>`,
|
|
95
96
|
' authorizationUrl:',
|
|
96
97
|
` ${started.authorizationUrl}`,
|
|
97
98
|
].join('\n');
|
|
@@ -99,7 +100,7 @@ export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<s
|
|
|
99
100
|
if (sub === 'finish' || (sub === 'login' && rest[1]?.toLowerCase() === 'finish')) {
|
|
100
101
|
const provider = sub === 'finish' ? rest[0] : rest[0];
|
|
101
102
|
const codeInput = sub === 'finish' ? rest[1] : rest[2];
|
|
102
|
-
if (!provider || !codeInput) return
|
|
103
|
+
if (!provider || !codeInput) return `Usage: ${binary} subscription login <provider> finish <code-or-url>`;
|
|
103
104
|
const resolved = getSubscriptionProviderConfig(provider, services.serviceRegistry.get(provider));
|
|
104
105
|
if (!resolved) return `No subscription provider found: ${provider}`;
|
|
105
106
|
const code = extractAuthorizationCode(codeInput);
|
|
@@ -127,7 +128,7 @@ export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<s
|
|
|
127
128
|
}
|
|
128
129
|
if (sub === 'refresh') {
|
|
129
130
|
const provider = rest[0];
|
|
130
|
-
if (!provider) return
|
|
131
|
+
if (!provider) return `Usage: ${binary} subscription refresh <provider>`;
|
|
131
132
|
const resolved = getSubscriptionProviderConfig(provider, services.serviceRegistry.get(provider));
|
|
132
133
|
if (!resolved) return `No subscription provider found: ${provider}`;
|
|
133
134
|
const record = await services.subscriptionManager.refreshOAuthToken(provider, resolved.oauth);
|
|
@@ -135,12 +136,12 @@ export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<s
|
|
|
135
136
|
}
|
|
136
137
|
if (sub === 'logout' || sub === 'remove') {
|
|
137
138
|
const provider = rest[0];
|
|
138
|
-
if (!provider) return
|
|
139
|
+
if (!provider) return `Usage: ${binary} subscription logout <provider>`;
|
|
139
140
|
const removed = services.subscriptionManager.logout(provider);
|
|
140
141
|
return removed ? `Subscription removed: ${provider}` : `No stored subscription session existed for ${provider}.`;
|
|
141
142
|
}
|
|
142
143
|
if (sub !== 'list' && sub !== 'status' && sub !== 'review') {
|
|
143
|
-
return
|
|
144
|
+
return `Usage: ${binary} subscription [list|providers|inspect <provider>|login <provider> start|finish <code-or-url>|refresh <provider>|logout <provider>]`;
|
|
144
145
|
}
|
|
145
146
|
const value = {
|
|
146
147
|
subscriptions: subscriptions.map((sub) => ({
|
|
@@ -180,7 +181,7 @@ export async function handleSecrets(runtime: CliCommandRuntime): Promise<string>
|
|
|
180
181
|
if (sub === 'test') {
|
|
181
182
|
const ref = rest.join(' ').trim();
|
|
182
183
|
if (!ref || !ref.startsWith('goodvibes://secrets/') || !isSecretRefInput(ref)) {
|
|
183
|
-
return
|
|
184
|
+
return `Usage: ${runtime.cli.binary} secrets test goodvibes://secrets/<source>/...`;
|
|
184
185
|
}
|
|
185
186
|
const resolved = await resolveSecretRef(ref, { resolveLocalSecret: (key) => secrets.get(key) });
|
|
186
187
|
const value = { ref: describeSecretRef(ref), resolved: Boolean(resolved.value) };
|
|
@@ -191,7 +192,7 @@ export async function handleSecrets(runtime: CliCommandRuntime): Promise<string>
|
|
|
191
192
|
const values = rest.filter((arg) => !arg.startsWith('--'));
|
|
192
193
|
const [key, ...rawValueParts] = values;
|
|
193
194
|
const value = rawValueParts.join(' ');
|
|
194
|
-
if (!key || !value) return `Usage:
|
|
195
|
+
if (!key || !value) return `Usage: ${runtime.cli.binary} secrets ${sub} <KEY> <value> [--user|--project] [--secure|--plaintext]`;
|
|
195
196
|
if (sub === 'link' && (!value.startsWith('goodvibes://secrets/') || !isSecretRefInput(value))) {
|
|
196
197
|
return 'Invalid secret reference. Use goodvibes://secrets/<source>/...';
|
|
197
198
|
}
|
|
@@ -203,7 +204,7 @@ export async function handleSecrets(runtime: CliCommandRuntime): Promise<string>
|
|
|
203
204
|
}
|
|
204
205
|
if (sub === 'delete') {
|
|
205
206
|
const key = rest.find((arg) => !arg.startsWith('--'));
|
|
206
|
-
if (!key) return
|
|
207
|
+
if (!key) return `Usage: ${runtime.cli.binary} secrets delete <KEY> [--user|--project] [--secure|--plaintext]`;
|
|
207
208
|
const flags = new Set(rest.filter((arg) => arg.startsWith('--')));
|
|
208
209
|
await secrets.delete(key, {
|
|
209
210
|
scope: flags.has('--user') ? 'user' : flags.has('--project') ? 'project' : undefined,
|
|
@@ -226,6 +227,7 @@ export async function handleSecrets(runtime: CliCommandRuntime): Promise<string>
|
|
|
226
227
|
|
|
227
228
|
export async function handleSessions(runtime: CliCommandRuntime): Promise<string | null> {
|
|
228
229
|
return await withRuntimeServices(runtime, (services) => {
|
|
230
|
+
const binary = runtime.cli.binary;
|
|
229
231
|
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
230
232
|
const sessions = services.sessionManager.list();
|
|
231
233
|
if (sub === 'list') {
|
|
@@ -237,7 +239,7 @@ export async function handleSessions(runtime: CliCommandRuntime): Promise<string
|
|
|
237
239
|
}
|
|
238
240
|
if (sub === 'show' || sub === 'info') {
|
|
239
241
|
const target = rest.join(' ').trim();
|
|
240
|
-
if (!target) return
|
|
242
|
+
if (!target) return `Usage: ${binary} sessions show <id|name>`;
|
|
241
243
|
const found = sessions.find((session) => session.name === target || session.name.startsWith(target) || session.title.toLowerCase() === target.toLowerCase());
|
|
242
244
|
if (!found) return `Session not found: ${target}`;
|
|
243
245
|
return formatJsonOrText(runtime.cli)(found, [
|
|
@@ -252,7 +254,7 @@ export async function handleSessions(runtime: CliCommandRuntime): Promise<string
|
|
|
252
254
|
if (sub === 'export') {
|
|
253
255
|
const target = rest[0];
|
|
254
256
|
const outputPath = rest[1];
|
|
255
|
-
if (!target) return
|
|
257
|
+
if (!target) return `Usage: ${binary} sessions export <id|name> [path]`;
|
|
256
258
|
const found = sessions.find((session) => session.name === target || session.name.startsWith(target) || session.title.toLowerCase() === target.toLowerCase());
|
|
257
259
|
if (!found) return `Session not found: ${target}`;
|
|
258
260
|
const data = services.sessionManager.load(found.name);
|
|
@@ -267,9 +269,9 @@ export async function handleSessions(runtime: CliCommandRuntime): Promise<string
|
|
|
267
269
|
}
|
|
268
270
|
if (sub === 'resume') {
|
|
269
271
|
const target = rest.join(' ').trim();
|
|
270
|
-
return target ? null :
|
|
272
|
+
return target ? null : `Usage: ${binary} sessions resume <id|name>`;
|
|
271
273
|
}
|
|
272
|
-
return
|
|
274
|
+
return `Usage: ${binary} sessions list|show <id>|export <id> [path]|resume <id>`;
|
|
273
275
|
});
|
|
274
276
|
}
|
|
275
277
|
|
|
@@ -291,11 +293,11 @@ export async function handleTasks(runtime: CliCommandRuntime): Promise<string> {
|
|
|
291
293
|
: ['GoodVibes tasks', ...tasks.map((task) => ` ${task.id} ${task.status} ${task.kind} ${task.title}`)].join('\n');
|
|
292
294
|
}
|
|
293
295
|
if (sub === 'show') {
|
|
294
|
-
if (!rest[0]) return
|
|
296
|
+
if (!rest[0]) return `Usage: ${runtime.cli.binary} tasks show <taskId>`;
|
|
295
297
|
const task = tasks.find((candidate) => candidate.id === rest[0]);
|
|
296
298
|
return task ? JSON.stringify(task, null, 2) : `Unknown task: ${rest[0] ?? ''}`;
|
|
297
299
|
}
|
|
298
|
-
return
|
|
300
|
+
return `Usage: ${runtime.cli.binary} tasks list|show <taskId>`;
|
|
299
301
|
});
|
|
300
302
|
}
|
|
301
303
|
|
package/src/cli/management.ts
CHANGED
|
@@ -244,7 +244,7 @@ export function readAuthPaths(runtime: CliCommandRuntime) {
|
|
|
244
244
|
export async function runNonInteractiveAgent(runtime: CliCommandRuntime): Promise<number> {
|
|
245
245
|
const prompt = runtime.cli.flags.prompt ?? runtime.cli.positionals.join(' ').trim();
|
|
246
246
|
if (!prompt) {
|
|
247
|
-
console.error(
|
|
247
|
+
console.error(`Usage: ${runtime.cli.binary} run|exec [prompt]`);
|
|
248
248
|
return 2;
|
|
249
249
|
}
|
|
250
250
|
|
|
@@ -329,6 +329,7 @@ export async function runNonInteractiveAgent(runtime: CliCommandRuntime): Promis
|
|
|
329
329
|
|
|
330
330
|
async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
|
|
331
331
|
return await withRuntimeServices(runtime, async (services) => {
|
|
332
|
+
const binary = runtime.cli.binary;
|
|
332
333
|
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
333
334
|
const snapshots = await listProviderRuntimeSnapshots(services.providerRegistry);
|
|
334
335
|
const current = services.providerRegistry.getCurrentModel();
|
|
@@ -354,7 +355,7 @@ async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
|
|
|
354
355
|
}
|
|
355
356
|
if (sub === 'use' || sub === 'set') {
|
|
356
357
|
const provider = rest[0];
|
|
357
|
-
if (!provider) return
|
|
358
|
+
if (!provider) return `Usage: ${binary} providers use <provider> [modelRegistryKey]`;
|
|
358
359
|
const providerModels = services.providerRegistry
|
|
359
360
|
.getSelectableModels()
|
|
360
361
|
.filter((model) => model.provider === provider || model.registryKey.startsWith(`${provider}:`));
|
|
@@ -377,7 +378,7 @@ async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
|
|
|
377
378
|
}
|
|
378
379
|
if (sub === 'inspect' || sub === 'show') {
|
|
379
380
|
const provider = rest[0];
|
|
380
|
-
if (!provider) return
|
|
381
|
+
if (!provider) return `Usage: ${binary} providers inspect <provider>`;
|
|
381
382
|
const snapshot = snapshots.find((candidate) => candidate.providerId === provider);
|
|
382
383
|
if (!snapshot) return `No provider found: ${provider}`;
|
|
383
384
|
const setup = classifyProviderSetup({
|
|
@@ -404,7 +405,7 @@ async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
|
|
|
404
405
|
` detail: ${snapshot.runtime.auth?.detail ?? snapshot.runtime.notes?.join('; ') ?? ''}`,
|
|
405
406
|
].join('\n'));
|
|
406
407
|
}
|
|
407
|
-
if (sub !== 'list') return
|
|
408
|
+
if (sub !== 'list') return `Usage: ${binary} providers [list|current|inspect <provider>|use <provider> [modelRegistryKey]]`;
|
|
408
409
|
const value = snapshots.map((snapshot) => ({
|
|
409
410
|
...classifyProviderSetup({
|
|
410
411
|
providerId: snapshot.providerId,
|
|
@@ -433,6 +434,7 @@ async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
|
|
|
433
434
|
|
|
434
435
|
async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
435
436
|
return await withRuntimeServices(runtime, async (services) => {
|
|
437
|
+
const binary = runtime.cli.binary;
|
|
436
438
|
const [subOrFilter, ...rest] = runtime.cli.commandArgs;
|
|
437
439
|
const current = services.providerRegistry.getCurrentModel().registryKey;
|
|
438
440
|
const providerSnapshots = await listProviderRuntimeSnapshots(services.providerRegistry);
|
|
@@ -469,7 +471,7 @@ async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
|
469
471
|
}
|
|
470
472
|
if (subOrFilter === 'use' || subOrFilter === 'set') {
|
|
471
473
|
const modelKey = rest[0];
|
|
472
|
-
if (!modelKey) return
|
|
474
|
+
if (!modelKey) return `Usage: ${binary} models use <registryKey>`;
|
|
473
475
|
const model = services.providerRegistry
|
|
474
476
|
.getSelectableModels()
|
|
475
477
|
.find((candidate) => candidate.registryKey === modelKey || candidate.id === modelKey);
|
|
@@ -485,7 +487,7 @@ async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
|
485
487
|
}
|
|
486
488
|
if (subOrFilter === 'pin' || subOrFilter === 'unpin') {
|
|
487
489
|
const modelKey = rest[0];
|
|
488
|
-
if (!modelKey) return `Usage:
|
|
490
|
+
if (!modelKey) return `Usage: ${binary} models ${subOrFilter} <registryKey>`;
|
|
489
491
|
if (subOrFilter === 'pin') await services.favoritesStore.pinModel(modelKey);
|
|
490
492
|
else await services.favoritesStore.unpinModel(modelKey);
|
|
491
493
|
return `Model ${subOrFilter === 'pin' ? 'pinned' : 'unpinned'}: ${modelKey}`;
|
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.92';
|
|
10
10
|
let _sdkVersion = '0.33.35';
|
|
11
11
|
try {
|
|
12
12
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {
|