@ivotoby/postgram-cli 1.29.0 → 1.30.1
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/README.md +18 -0
- package/dist/client.js +6 -0
- package/dist/pgm.js +181 -22
- package/dist/search-output.js +170 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -30,6 +30,16 @@ pgm store "decided to use pgvector" --type memory --tags "decisions,architecture
|
|
|
30
30
|
# Search
|
|
31
31
|
pgm search "pgvector decisions" --limit 5
|
|
32
32
|
|
|
33
|
+
# Agent-friendly output formats
|
|
34
|
+
pgm search "pgvector decisions" --json # compact JSON by default
|
|
35
|
+
pgm search "pgvector decisions" --json --full-response # full API-shaped JSON
|
|
36
|
+
pgm search "pgvector decisions" --toon # compact TOON output
|
|
37
|
+
pgm list --json # compact JSON rows
|
|
38
|
+
pgm list --json --full-response # full API-shaped rows
|
|
39
|
+
pgm list --toon # compact TOON rows
|
|
40
|
+
pgm expand <entity-id> --json # compact graph JSON
|
|
41
|
+
pgm expand <entity-id> --toon # compact TOON graph
|
|
42
|
+
|
|
33
43
|
# Recall by ID
|
|
34
44
|
pgm recall <entity-id>
|
|
35
45
|
|
|
@@ -63,6 +73,14 @@ pgm queue
|
|
|
63
73
|
pgm store "hello" --json
|
|
64
74
|
```
|
|
65
75
|
|
|
76
|
+
Agent-facing `--json` output is compact by default for search, list, task list,
|
|
77
|
+
graph expansion, write acknowledgements, and link acknowledgements. It omits
|
|
78
|
+
token-heavy fields such as timestamps, metadata, nested `entity` objects, and
|
|
79
|
+
raw similarity unless you pass `--full-response`. Use `--toon` on list-like
|
|
80
|
+
commands (`search`, `list`, `task list`, `expand`) when an agent needs the
|
|
81
|
+
smallest readable output. TOON and compacting are CLI-layer formats; the
|
|
82
|
+
Postgram API remains JSON.
|
|
83
|
+
|
|
66
84
|
## Memory Roles
|
|
67
85
|
|
|
68
86
|
Store durable memory:
|
package/dist/client.js
CHANGED
|
@@ -49,6 +49,12 @@ export function createPgmClient(options) {
|
|
|
49
49
|
body: input
|
|
50
50
|
});
|
|
51
51
|
},
|
|
52
|
+
groomSessionContext(input) {
|
|
53
|
+
return request(options, '/api/memory/session-context/groom', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
body: input
|
|
56
|
+
});
|
|
57
|
+
},
|
|
52
58
|
recallEntity(id, input = {}) {
|
|
53
59
|
const params = new URLSearchParams();
|
|
54
60
|
if (input.owner) {
|
package/dist/pgm.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createPgmClient } from './client.js';
|
|
|
9
9
|
import { handleCliFailure, isJsonMode, parseCommaList, parseJsonObject, printHuman, printJson, readStdinText, resolvePgmConfig, shortId } from './shared.js';
|
|
10
10
|
import { AppError, ErrorCode } from './errors.js';
|
|
11
11
|
import { buildSyncManifest } from './sync-walk.js';
|
|
12
|
+
import { compactEdgeResponse, compactEntityListResponse, compactGraphResponse, compactSearchResponse, compactStoredEntityResponse, entityListResponseToToon, graphResponseToToon, searchResponseToToon } from './search-output.js';
|
|
12
13
|
function formatStoredEntity(entity) {
|
|
13
14
|
return [
|
|
14
15
|
`${entity.type} ${shortId(entity.id)}${entity.status ? ` [${entity.status}]` : ''}`,
|
|
@@ -26,7 +27,8 @@ function formatSearchResults(results) {
|
|
|
26
27
|
for (const result of results) {
|
|
27
28
|
lines.push(`${result.entity.type} ${shortId(result.entity.id)} score=${result.score.toFixed(3)}`);
|
|
28
29
|
lines.push(` ${result.chunk_content}`);
|
|
29
|
-
if (result.entity.content &&
|
|
30
|
+
if (result.entity.content &&
|
|
31
|
+
result.entity.content !== result.chunk_content) {
|
|
30
32
|
lines.push(` entity: ${result.entity.content}`);
|
|
31
33
|
}
|
|
32
34
|
if (result.related && result.related.length > 0) {
|
|
@@ -57,6 +59,48 @@ function formatTaskList(items) {
|
|
|
57
59
|
];
|
|
58
60
|
});
|
|
59
61
|
}
|
|
62
|
+
function validateToonOptions(options, json) {
|
|
63
|
+
if (options.toon === true && (json || options.fullResponse === true)) {
|
|
64
|
+
throw new AppError(ErrorCode.VALIDATION, '--toon cannot be combined with --json or --full-response');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function parseDurationMs(value) {
|
|
68
|
+
const match = /^(\d+)([mhd])$/i.exec(value.trim());
|
|
69
|
+
if (!match) {
|
|
70
|
+
throw new AppError(ErrorCode.VALIDATION, `Invalid duration '${value}'. Use format like '15m', '2h', or '7d'.`);
|
|
71
|
+
}
|
|
72
|
+
const [, amountText, unitText] = match;
|
|
73
|
+
const amount = Number(amountText);
|
|
74
|
+
const unit = unitText.toLowerCase();
|
|
75
|
+
const milliseconds = unit === 'm'
|
|
76
|
+
? amount * 60 * 1000
|
|
77
|
+
: unit === 'h'
|
|
78
|
+
? amount * 60 * 60 * 1000
|
|
79
|
+
: amount * 24 * 60 * 60 * 1000;
|
|
80
|
+
if (milliseconds > 3650 * 24 * 60 * 60 * 1000) {
|
|
81
|
+
throw new AppError(ErrorCode.VALIDATION, `Duration '${value}' exceeds the maximum allowed (3650 days / ~10 years).`);
|
|
82
|
+
}
|
|
83
|
+
return milliseconds;
|
|
84
|
+
}
|
|
85
|
+
function formatGroomCandidates(candidates) {
|
|
86
|
+
if (candidates.length === 0) {
|
|
87
|
+
return ['No eligible session-context memories'];
|
|
88
|
+
}
|
|
89
|
+
const lines = [`Would archive ${candidates.length} session-context memories`];
|
|
90
|
+
for (const candidate of candidates) {
|
|
91
|
+
const metadata = candidate.metadata;
|
|
92
|
+
lines.push([
|
|
93
|
+
` ${shortId(candidate.id)} ${candidate.createdAt.slice(0, 10)}`,
|
|
94
|
+
candidate.tags.length ? `tags=${candidate.tags.join(',')}` : 'tags=-',
|
|
95
|
+
metadata.topic ? `topic=${metadata.topic}` : undefined,
|
|
96
|
+
metadata.session_id ? `session=${metadata.session_id}` : undefined,
|
|
97
|
+
candidate.content ? `content=${candidate.content}` : 'content=-'
|
|
98
|
+
]
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.join(' | '));
|
|
101
|
+
}
|
|
102
|
+
return lines;
|
|
103
|
+
}
|
|
60
104
|
async function resolveStoreContent(content) {
|
|
61
105
|
if (content !== undefined) {
|
|
62
106
|
return content;
|
|
@@ -209,6 +253,7 @@ program
|
|
|
209
253
|
.option('--source <source>', 'entity source')
|
|
210
254
|
.option('--metadata <json>', 'JSON metadata object')
|
|
211
255
|
.option('--skip-extraction', 'store without ever queueing graph extraction')
|
|
256
|
+
.option('--full-response', 'emit the full API response when used with --json')
|
|
212
257
|
.action(async (content, options, command) => {
|
|
213
258
|
await runWithClient(command, async (client, json) => {
|
|
214
259
|
const body = await client.storeEntity({
|
|
@@ -223,7 +268,9 @@ program
|
|
|
223
268
|
skip_extraction: options.skipExtraction === true ? true : undefined
|
|
224
269
|
});
|
|
225
270
|
return json
|
|
226
|
-
?
|
|
271
|
+
? options.fullResponse === true
|
|
272
|
+
? body
|
|
273
|
+
: compactStoredEntityResponse(body)
|
|
227
274
|
: formatStoredEntity({
|
|
228
275
|
id: body.entity.id,
|
|
229
276
|
type: body.entity.type,
|
|
@@ -249,9 +296,13 @@ program
|
|
|
249
296
|
.option('--expand-graph', 'include graph-connected entities in results')
|
|
250
297
|
.option('--include-archived', 'include archived entities in results')
|
|
251
298
|
.option('--memory-role <role>', 'memory role filter: durable_memory or session_context')
|
|
252
|
-
.option('--
|
|
299
|
+
.option('--full-response', 'emit the full API response instead of compact default output when used with --json')
|
|
300
|
+
.option('--toon', 'emit compact TOON output for lower agent token use; formatting is applied in the CLI, not the API')
|
|
253
301
|
.action(async (query, options, command) => {
|
|
254
302
|
await runWithClient(command, async (client, json) => {
|
|
303
|
+
if (options.toon === true && (json || options.fullResponse === true)) {
|
|
304
|
+
throw new AppError(ErrorCode.VALIDATION, '--toon cannot be combined with --json or --full-response');
|
|
305
|
+
}
|
|
255
306
|
if (options.memoryRole !== undefined &&
|
|
256
307
|
!['durable_memory', 'session_context'].includes(options.memoryRole)) {
|
|
257
308
|
throw new AppError(ErrorCode.VALIDATION, '--memory-role must be durable_memory or session_context');
|
|
@@ -267,15 +318,66 @@ program
|
|
|
267
318
|
recency_weight: Number(options.recencyWeight),
|
|
268
319
|
expand_graph: options.expandGraph === true ? true : undefined,
|
|
269
320
|
include_archived: options.includeArchived === true ? true : undefined,
|
|
270
|
-
memory_role: options.memoryRole
|
|
271
|
-
include_other_clients_session_context: options.includeOtherClientsSessionContext === true ? true : undefined
|
|
321
|
+
memory_role: options.memoryRole
|
|
272
322
|
});
|
|
273
|
-
|
|
323
|
+
if (options.toon === true) {
|
|
324
|
+
return searchResponseToToon(compactSearchResponse(body));
|
|
325
|
+
}
|
|
326
|
+
if (json) {
|
|
327
|
+
return options.fullResponse === true
|
|
328
|
+
? body
|
|
329
|
+
: compactSearchResponse(body);
|
|
330
|
+
}
|
|
331
|
+
return formatSearchResults(body.results);
|
|
274
332
|
});
|
|
275
333
|
});
|
|
276
334
|
const memoryCommand = program
|
|
277
335
|
.command('memory')
|
|
278
336
|
.description('Memory-specific commands');
|
|
337
|
+
memoryCommand
|
|
338
|
+
.command('groom')
|
|
339
|
+
.description('Preview or archive stale session-context memories for the authenticated client')
|
|
340
|
+
.option('--dry-run', 'preview without mutating')
|
|
341
|
+
.option('--older-than <duration>', 'only include memories older than this (e.g. 15m, 2h, 7d)', '7d')
|
|
342
|
+
.option('--limit <limit>', 'maximum candidates', '50')
|
|
343
|
+
.option('--topic <topic>', 'filter by topic')
|
|
344
|
+
.option('--session-id <sessionId>', 'filter by session id')
|
|
345
|
+
.option('--tag <tag>', 'filter by tag (repeatable)', (value, previous = []) => {
|
|
346
|
+
previous.push(value);
|
|
347
|
+
return previous;
|
|
348
|
+
}, [])
|
|
349
|
+
.option('--yes', 'confirm archive mutation')
|
|
350
|
+
.action(async (options, command) => {
|
|
351
|
+
await runWithClient(command, async (client, json) => {
|
|
352
|
+
const limit = Number.parseInt(options.limit, 10);
|
|
353
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
354
|
+
throw new AppError(ErrorCode.VALIDATION, '--limit must be a positive integer');
|
|
355
|
+
}
|
|
356
|
+
if (options.dryRun !== true && options.yes !== true) {
|
|
357
|
+
throw new AppError(ErrorCode.VALIDATION, '--yes is required outside dry-run');
|
|
358
|
+
}
|
|
359
|
+
const body = await client.groomSessionContext({
|
|
360
|
+
dry_run: options.dryRun === true,
|
|
361
|
+
confirmed: options.dryRun === true ? undefined : true,
|
|
362
|
+
older_than_ms: parseDurationMs(options.olderThan),
|
|
363
|
+
limit,
|
|
364
|
+
topic: options.topic,
|
|
365
|
+
session_id: options.sessionId,
|
|
366
|
+
tags: options.tag
|
|
367
|
+
});
|
|
368
|
+
if (json) {
|
|
369
|
+
return body;
|
|
370
|
+
}
|
|
371
|
+
if (body.dryRun) {
|
|
372
|
+
return formatGroomCandidates(body.eligible ?? []);
|
|
373
|
+
}
|
|
374
|
+
return [
|
|
375
|
+
`Archived ${body.archived} session-context memories`,
|
|
376
|
+
` promoted: ${body.promoted}`,
|
|
377
|
+
` skipped: ${body.skipped}`
|
|
378
|
+
];
|
|
379
|
+
});
|
|
380
|
+
});
|
|
279
381
|
memoryCommand
|
|
280
382
|
.command('session-context')
|
|
281
383
|
.alias('session')
|
|
@@ -290,6 +392,7 @@ memoryCommand
|
|
|
290
392
|
.option('--promotable', 'mark this session context as promotable')
|
|
291
393
|
.option('--groom-after <groomAfter>', 'ISO timestamp after which grooming may archive/promote it')
|
|
292
394
|
.option('--expires-at <expiresAt>', 'ISO timestamp after which the context is stale')
|
|
395
|
+
.option('--full-response', 'emit the full API response when used with --json')
|
|
293
396
|
.action(async (content, options, command) => {
|
|
294
397
|
await runWithClient(command, async (client, json) => {
|
|
295
398
|
const body = await client.storeSessionContext({
|
|
@@ -305,7 +408,9 @@ memoryCommand
|
|
|
305
408
|
expires_at: options.expiresAt
|
|
306
409
|
});
|
|
307
410
|
return json
|
|
308
|
-
?
|
|
411
|
+
? options.fullResponse === true
|
|
412
|
+
? body
|
|
413
|
+
: compactStoredEntityResponse(body)
|
|
309
414
|
: formatStoredEntity({
|
|
310
415
|
id: body.entity.id,
|
|
311
416
|
type: body.entity.type,
|
|
@@ -341,8 +446,11 @@ program
|
|
|
341
446
|
.option('--limit <limit>', 'result limit', '50')
|
|
342
447
|
.option('--offset <offset>', 'result offset', '0')
|
|
343
448
|
.option('--include-archived', 'include archived entities')
|
|
449
|
+
.option('--full-response', 'emit the full API response instead of compact default output when used with --json')
|
|
450
|
+
.option('--toon', 'emit compact TOON output for lower agent token use; formatting is applied in the CLI, not the API')
|
|
344
451
|
.action(async (options, command) => {
|
|
345
452
|
await runWithClient(command, async (client, json) => {
|
|
453
|
+
validateToonOptions(options, json);
|
|
346
454
|
const body = await client.listEntities({
|
|
347
455
|
type: options.type,
|
|
348
456
|
status: options.status,
|
|
@@ -353,8 +461,13 @@ program
|
|
|
353
461
|
offset: Number(options.offset),
|
|
354
462
|
include_archived: options.includeArchived === true ? true : undefined
|
|
355
463
|
});
|
|
464
|
+
if (options.toon === true) {
|
|
465
|
+
return entityListResponseToToon(compactEntityListResponse(body));
|
|
466
|
+
}
|
|
356
467
|
if (json) {
|
|
357
|
-
return
|
|
468
|
+
return options.fullResponse === true
|
|
469
|
+
? body
|
|
470
|
+
: compactEntityListResponse(body);
|
|
358
471
|
}
|
|
359
472
|
if (body.items.length === 0) {
|
|
360
473
|
return ['No entities'];
|
|
@@ -387,6 +500,7 @@ program
|
|
|
387
500
|
.option('--metadata <json>', 'JSON metadata object')
|
|
388
501
|
.option('--version <version>', 'expected version')
|
|
389
502
|
.option('--force', 'retry using the latest version on conflict')
|
|
503
|
+
.option('--full-response', 'emit the full API response when used with --json')
|
|
390
504
|
.action(async (id, options, command) => {
|
|
391
505
|
await runWithClient(command, async (client, json) => {
|
|
392
506
|
const payload = {
|
|
@@ -412,7 +526,11 @@ program
|
|
|
412
526
|
else {
|
|
413
527
|
throw new Error('--version is required unless --force is set');
|
|
414
528
|
}
|
|
415
|
-
return json
|
|
529
|
+
return json
|
|
530
|
+
? options.fullResponse === true
|
|
531
|
+
? body
|
|
532
|
+
: compactStoredEntityResponse(body)
|
|
533
|
+
: formatStoredEntity(body.entity);
|
|
416
534
|
});
|
|
417
535
|
});
|
|
418
536
|
program
|
|
@@ -436,6 +554,7 @@ taskCommand
|
|
|
436
554
|
.option('--tags <tags>', 'comma-separated tags')
|
|
437
555
|
.option('--visibility <visibility>', 'task visibility', 'shared')
|
|
438
556
|
.option('--metadata <json>', 'JSON metadata object')
|
|
557
|
+
.option('--full-response', 'emit the full API response when used with --json')
|
|
439
558
|
.action(async (content, options, command) => {
|
|
440
559
|
await runWithClient(command, async (client, json) => {
|
|
441
560
|
const taskContent = await resolveStoreContent(content);
|
|
@@ -449,7 +568,9 @@ taskCommand
|
|
|
449
568
|
metadata: parseJsonObject(options.metadata)
|
|
450
569
|
});
|
|
451
570
|
return json
|
|
452
|
-
?
|
|
571
|
+
? options.fullResponse === true
|
|
572
|
+
? body
|
|
573
|
+
: compactStoredEntityResponse(body)
|
|
453
574
|
: formatStoredEntity({
|
|
454
575
|
id: body.entity.id,
|
|
455
576
|
type: body.entity.type,
|
|
@@ -469,8 +590,11 @@ taskCommand
|
|
|
469
590
|
.option('--limit <limit>', 'result limit', '50')
|
|
470
591
|
.option('--offset <offset>', 'result offset', '0')
|
|
471
592
|
.option('--include-archived', 'include archived tasks')
|
|
593
|
+
.option('--full-response', 'emit the full API response instead of compact default output when used with --json')
|
|
594
|
+
.option('--toon', 'emit compact TOON output for lower agent token use; formatting is applied in the CLI, not the API')
|
|
472
595
|
.action(async (options, command) => {
|
|
473
596
|
await runWithClient(command, async (client, json) => {
|
|
597
|
+
validateToonOptions(options, json);
|
|
474
598
|
const body = await client.listTasks({
|
|
475
599
|
status: options.status,
|
|
476
600
|
context: options.context,
|
|
@@ -478,7 +602,14 @@ taskCommand
|
|
|
478
602
|
offset: Number(options.offset),
|
|
479
603
|
include_archived: options.includeArchived === true ? true : undefined
|
|
480
604
|
});
|
|
481
|
-
|
|
605
|
+
if (options.toon === true) {
|
|
606
|
+
return entityListResponseToToon(compactEntityListResponse(body));
|
|
607
|
+
}
|
|
608
|
+
return json
|
|
609
|
+
? options.fullResponse === true
|
|
610
|
+
? body
|
|
611
|
+
: compactEntityListResponse(body)
|
|
612
|
+
: formatTaskList(body.items);
|
|
482
613
|
});
|
|
483
614
|
});
|
|
484
615
|
taskCommand
|
|
@@ -493,6 +624,7 @@ taskCommand
|
|
|
493
624
|
.option('--visibility <visibility>', 'updated task visibility')
|
|
494
625
|
.option('--metadata <json>', 'JSON metadata object')
|
|
495
626
|
.option('--version <version>', 'expected version', '')
|
|
627
|
+
.option('--full-response', 'emit the full API response when used with --json')
|
|
496
628
|
.action(async (id, options, command) => {
|
|
497
629
|
await runWithClient(command, async (client, json) => {
|
|
498
630
|
if (!options.version) {
|
|
@@ -509,7 +641,9 @@ taskCommand
|
|
|
509
641
|
metadata: parseJsonObject(options.metadata)
|
|
510
642
|
});
|
|
511
643
|
return json
|
|
512
|
-
?
|
|
644
|
+
? options.fullResponse === true
|
|
645
|
+
? body
|
|
646
|
+
: compactStoredEntityResponse(body)
|
|
513
647
|
: formatStoredEntity({
|
|
514
648
|
id: body.entity.id,
|
|
515
649
|
type: body.entity.type,
|
|
@@ -526,6 +660,7 @@ taskCommand
|
|
|
526
660
|
.description('Mark a task complete')
|
|
527
661
|
.argument('id', 'task ID')
|
|
528
662
|
.option('--version <version>', 'expected version')
|
|
663
|
+
.option('--full-response', 'emit the full API response when used with --json')
|
|
529
664
|
.action(async (id, options, command) => {
|
|
530
665
|
await runWithClient(command, async (client, json) => {
|
|
531
666
|
if (options.version === undefined) {
|
|
@@ -533,7 +668,9 @@ taskCommand
|
|
|
533
668
|
}
|
|
534
669
|
const body = await client.completeTask(id, Number(options.version));
|
|
535
670
|
return json
|
|
536
|
-
?
|
|
671
|
+
? options.fullResponse === true
|
|
672
|
+
? body
|
|
673
|
+
: compactStoredEntityResponse(body)
|
|
537
674
|
: formatStoredEntity({
|
|
538
675
|
id: body.entity.id,
|
|
539
676
|
type: body.entity.type,
|
|
@@ -554,7 +691,9 @@ program
|
|
|
554
691
|
if (json)
|
|
555
692
|
return body;
|
|
556
693
|
const e = body.embedding;
|
|
557
|
-
const age = e.oldest_pending_secs !== null
|
|
694
|
+
const age = e.oldest_pending_secs !== null
|
|
695
|
+
? ` oldest_pending=${e.oldest_pending_secs}s`
|
|
696
|
+
: '';
|
|
558
697
|
const lines = [
|
|
559
698
|
`embedding: pending=${e.pending} completed=${e.completed} failed=${e.failed} retry_eligible=${e.retry_eligible}${age}`
|
|
560
699
|
];
|
|
@@ -603,8 +742,14 @@ program
|
|
|
603
742
|
const manifest = await buildSyncManifest(resolvedDir);
|
|
604
743
|
const config = await resolvePgmConfig();
|
|
605
744
|
const client = createPgmClient(config);
|
|
606
|
-
const manifestForServer = manifest.map(({ path: p, sha }) => ({
|
|
607
|
-
|
|
745
|
+
const manifestForServer = manifest.map(({ path: p, sha }) => ({
|
|
746
|
+
path: p,
|
|
747
|
+
sha
|
|
748
|
+
}));
|
|
749
|
+
const diff = await client.diffSync({
|
|
750
|
+
repo: repoName,
|
|
751
|
+
files: manifestForServer
|
|
752
|
+
});
|
|
608
753
|
if (options.dryRun) {
|
|
609
754
|
const newCount = diff.toUpload.filter((f) => f.reason === 'new').length;
|
|
610
755
|
const changedCount = diff.toUpload.filter((f) => f.reason === 'changed').length;
|
|
@@ -650,7 +795,8 @@ program
|
|
|
650
795
|
}
|
|
651
796
|
const content = await fsReadFile(entry.fullPath, 'utf8');
|
|
652
797
|
const size = Buffer.byteLength(content, 'utf8');
|
|
653
|
-
if (batch.length > 0 &&
|
|
798
|
+
if (batch.length > 0 &&
|
|
799
|
+
(batch.length >= BATCH_FILES || batchBytes + size > BATCH_BYTES)) {
|
|
654
800
|
await flushBatch();
|
|
655
801
|
}
|
|
656
802
|
batch.push({ path: toUpload.path, sha: toUpload.sha, content });
|
|
@@ -687,6 +833,7 @@ program
|
|
|
687
833
|
.argument('<target-id>', 'target entity ID')
|
|
688
834
|
.requiredOption('--relation <relation>', 'relationship type')
|
|
689
835
|
.option('--confidence <n>', 'confidence score 0-1', '1.0')
|
|
836
|
+
.option('--full-response', 'emit the full API response when used with --json')
|
|
690
837
|
.action(async (sourceId, targetId, options, command) => {
|
|
691
838
|
await runWithClient(command, async (client, json) => {
|
|
692
839
|
const body = await client.createEdge({
|
|
@@ -695,9 +842,12 @@ program
|
|
|
695
842
|
relation: options.relation,
|
|
696
843
|
confidence: Number(options.confidence)
|
|
697
844
|
});
|
|
698
|
-
if (json)
|
|
699
|
-
return body;
|
|
700
|
-
|
|
845
|
+
if (json) {
|
|
846
|
+
return options.fullResponse === true ? body : compactEdgeResponse(body);
|
|
847
|
+
}
|
|
848
|
+
return [
|
|
849
|
+
`Linked ${shortId(sourceId)} → ${shortId(targetId)} (${options.relation})`
|
|
850
|
+
];
|
|
701
851
|
});
|
|
702
852
|
});
|
|
703
853
|
program
|
|
@@ -717,16 +867,25 @@ program
|
|
|
717
867
|
.option('--depth <n>', 'traversal depth (1-3)', '1')
|
|
718
868
|
.option('--relation <types>', 'comma-separated relation types')
|
|
719
869
|
.option('--owner <owner>', 'owner filter')
|
|
870
|
+
.option('--full-response', 'emit the full API response instead of compact default output when used with --json')
|
|
871
|
+
.option('--toon', 'emit compact TOON output for lower agent token use; formatting is applied in the CLI, not the API')
|
|
720
872
|
.action(async (entityId, options, command) => {
|
|
721
873
|
await runWithClient(command, async (client, json) => {
|
|
874
|
+
validateToonOptions(options, json);
|
|
722
875
|
const relationTypes = parseCommaList(options.relation);
|
|
723
876
|
const body = await client.expandGraph(entityId, {
|
|
724
877
|
depth: Number(options.depth),
|
|
725
878
|
...(relationTypes !== undefined ? { relationTypes } : {}),
|
|
726
879
|
...(options.owner !== undefined ? { owner: options.owner } : {})
|
|
727
880
|
});
|
|
728
|
-
if (
|
|
729
|
-
return body;
|
|
881
|
+
if (options.toon === true) {
|
|
882
|
+
return graphResponseToToon(compactGraphResponse(body));
|
|
883
|
+
}
|
|
884
|
+
if (json) {
|
|
885
|
+
return options.fullResponse === true
|
|
886
|
+
? body
|
|
887
|
+
: compactGraphResponse(body);
|
|
888
|
+
}
|
|
730
889
|
const lines = [];
|
|
731
890
|
lines.push(`Graph for ${shortId(entityId)}:`);
|
|
732
891
|
lines.push(` ${body.entities.length} entities, ${body.edges.length} edges`);
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export function compactStoredEntity(entity, options = {}) {
|
|
2
|
+
return {
|
|
3
|
+
id: entity.id,
|
|
4
|
+
type: entity.type,
|
|
5
|
+
...(entity.version !== undefined ? { version: entity.version } : {}),
|
|
6
|
+
...(options.includeContent ? { content: entity.content } : {}),
|
|
7
|
+
...(entity.status ? { status: entity.status } : {}),
|
|
8
|
+
...(entity.visibility ? { visibility: entity.visibility } : {}),
|
|
9
|
+
...(entity.owner ? { owner: entity.owner } : {}),
|
|
10
|
+
...(entity.enrichment_status
|
|
11
|
+
? { enrichment_status: entity.enrichment_status }
|
|
12
|
+
: {}),
|
|
13
|
+
...(entity.tags?.length ? { tags: entity.tags } : {}),
|
|
14
|
+
...(entity.source ? { source: entity.source } : {})
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function compactStoredEntityResponse(response) {
|
|
18
|
+
return {
|
|
19
|
+
entity: compactStoredEntity(response.entity)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function compactEntityListResponse(response) {
|
|
23
|
+
return {
|
|
24
|
+
items: response.items.map((entity) => compactStoredEntity(entity, { includeContent: true })),
|
|
25
|
+
total: response.total,
|
|
26
|
+
limit: response.limit,
|
|
27
|
+
offset: response.offset
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function compactEdge(edge) {
|
|
31
|
+
return {
|
|
32
|
+
id: edge.id,
|
|
33
|
+
source_id: edge.source_id,
|
|
34
|
+
target_id: edge.target_id,
|
|
35
|
+
relation: edge.relation,
|
|
36
|
+
...(edge.confidence !== undefined ? { confidence: edge.confidence } : {})
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function compactEdgeResponse(response) {
|
|
40
|
+
return {
|
|
41
|
+
edge: compactEdge(response.edge)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function compactGraphResponse(response) {
|
|
45
|
+
return {
|
|
46
|
+
entities: response.entities.map((entity) => ({
|
|
47
|
+
id: entity.id,
|
|
48
|
+
type: entity.type,
|
|
49
|
+
content: entity.content
|
|
50
|
+
})),
|
|
51
|
+
edges: response.edges.map(compactEdge)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function compactSearchResponse(response) {
|
|
55
|
+
return {
|
|
56
|
+
results: response.results.map((entry) => ({
|
|
57
|
+
id: entry.entity.id,
|
|
58
|
+
type: entry.entity.type,
|
|
59
|
+
score: entry.score,
|
|
60
|
+
content: entry.entity.content,
|
|
61
|
+
chunk: entry.chunk_content,
|
|
62
|
+
...(entry.entity.tags?.length ? { tags: entry.entity.tags } : {}),
|
|
63
|
+
...(entry.related?.length
|
|
64
|
+
? {
|
|
65
|
+
related: entry.related.map((related) => ({
|
|
66
|
+
id: related.entity.id,
|
|
67
|
+
type: related.entity.type,
|
|
68
|
+
relation: related.relation,
|
|
69
|
+
direction: related.direction,
|
|
70
|
+
content: related.entity.content
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
73
|
+
: {})
|
|
74
|
+
}))
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function toonScalar(value) {
|
|
78
|
+
if (value === null || value === undefined) {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
let scalar;
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
scalar = value.join('|');
|
|
84
|
+
}
|
|
85
|
+
else if (typeof value === 'object') {
|
|
86
|
+
scalar = JSON.stringify(value);
|
|
87
|
+
}
|
|
88
|
+
else if (typeof value === 'string') {
|
|
89
|
+
scalar = value;
|
|
90
|
+
}
|
|
91
|
+
else if (typeof value === 'number' ||
|
|
92
|
+
typeof value === 'boolean' ||
|
|
93
|
+
typeof value === 'bigint') {
|
|
94
|
+
scalar = value.toString();
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
scalar = JSON.stringify(value);
|
|
98
|
+
}
|
|
99
|
+
return /[,\n\r"]/u.test(scalar) ? JSON.stringify(scalar) : scalar;
|
|
100
|
+
}
|
|
101
|
+
export function searchResponseToToon(response) {
|
|
102
|
+
const lines = [
|
|
103
|
+
`results[${response.results.length}]{id,type,score,content,chunk,tags,related}:`
|
|
104
|
+
];
|
|
105
|
+
for (const result of response.results) {
|
|
106
|
+
lines.push([
|
|
107
|
+
result.id,
|
|
108
|
+
result.type,
|
|
109
|
+
Number.isFinite(result.score)
|
|
110
|
+
? Number(result.score.toFixed(6))
|
|
111
|
+
: result.score,
|
|
112
|
+
result.content,
|
|
113
|
+
result.chunk,
|
|
114
|
+
result.tags,
|
|
115
|
+
result.related?.length ? `${result.related.length} related` : ''
|
|
116
|
+
]
|
|
117
|
+
.map(toonScalar)
|
|
118
|
+
.join(','));
|
|
119
|
+
if (result.related?.length) {
|
|
120
|
+
lines.push(` related[${result.related.length}]{id,type,relation,direction,content}:`);
|
|
121
|
+
for (const related of result.related) {
|
|
122
|
+
lines.push(` ${[
|
|
123
|
+
related.id,
|
|
124
|
+
related.type,
|
|
125
|
+
related.relation,
|
|
126
|
+
related.direction,
|
|
127
|
+
related.content
|
|
128
|
+
]
|
|
129
|
+
.map(toonScalar)
|
|
130
|
+
.join(',')}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|
|
136
|
+
export function entityListResponseToToon(response) {
|
|
137
|
+
const lines = [
|
|
138
|
+
`items[${response.items.length}]{id,type,status,version,content,tags,owner}:`
|
|
139
|
+
];
|
|
140
|
+
for (const item of response.items) {
|
|
141
|
+
lines.push([
|
|
142
|
+
item.id,
|
|
143
|
+
item.type,
|
|
144
|
+
item.status,
|
|
145
|
+
item.version,
|
|
146
|
+
item.content,
|
|
147
|
+
item.tags,
|
|
148
|
+
item.owner
|
|
149
|
+
]
|
|
150
|
+
.map(toonScalar)
|
|
151
|
+
.join(','));
|
|
152
|
+
}
|
|
153
|
+
lines.push(`total,${response.total}`);
|
|
154
|
+
lines.push(`limit,${response.limit}`);
|
|
155
|
+
lines.push(`offset,${response.offset}`);
|
|
156
|
+
return lines.join('\n');
|
|
157
|
+
}
|
|
158
|
+
export function graphResponseToToon(response) {
|
|
159
|
+
const lines = [`entities[${response.entities.length}]{id,type,content}:`];
|
|
160
|
+
for (const entity of response.entities) {
|
|
161
|
+
lines.push([entity.id, entity.type, entity.content].map(toonScalar).join(','));
|
|
162
|
+
}
|
|
163
|
+
lines.push(`edges[${response.edges.length}]{id,source_id,target_id,relation,confidence}:`);
|
|
164
|
+
for (const edge of response.edges) {
|
|
165
|
+
lines.push([edge.id, edge.source_id, edge.target_id, edge.relation, edge.confidence]
|
|
166
|
+
.map(toonScalar)
|
|
167
|
+
.join(','));
|
|
168
|
+
}
|
|
169
|
+
return lines.join('\n');
|
|
170
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ivotoby/postgram-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.1",
|
|
4
4
|
"description": "Postgram CLI — store, search, and manage entities from the command line",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
36
37
|
"prepublishOnly": "npm run build",
|
|
37
38
|
"test": "vitest run",
|
|
38
39
|
"test:watch": "vitest",
|