@pellux/goodvibes-agent 0.1.90 → 0.1.91

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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.91 - 2026-06-01
6
+
7
+ - efbb82a Expand Agent Knowledge CLI management
8
+
5
9
  ## 0.1.90 - 2026-06-01
6
10
 
7
11
  - 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.90",
3
+ "version": "0.1.91",
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
+ }
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: {
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.90';
9
+ let _version = '0.1.91';
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 {