@ivotoby/postgram-cli 1.29.0 → 1.30.0

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 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/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 && result.entity.content !== result.chunk_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,11 @@ 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
+ }
60
67
  async function resolveStoreContent(content) {
61
68
  if (content !== undefined) {
62
69
  return content;
@@ -209,6 +216,7 @@ program
209
216
  .option('--source <source>', 'entity source')
210
217
  .option('--metadata <json>', 'JSON metadata object')
211
218
  .option('--skip-extraction', 'store without ever queueing graph extraction')
219
+ .option('--full-response', 'emit the full API response when used with --json')
212
220
  .action(async (content, options, command) => {
213
221
  await runWithClient(command, async (client, json) => {
214
222
  const body = await client.storeEntity({
@@ -223,7 +231,9 @@ program
223
231
  skip_extraction: options.skipExtraction === true ? true : undefined
224
232
  });
225
233
  return json
226
- ? body
234
+ ? options.fullResponse === true
235
+ ? body
236
+ : compactStoredEntityResponse(body)
227
237
  : formatStoredEntity({
228
238
  id: body.entity.id,
229
239
  type: body.entity.type,
@@ -250,8 +260,13 @@ program
250
260
  .option('--include-archived', 'include archived entities in results')
251
261
  .option('--memory-role <role>', 'memory role filter: durable_memory or session_context')
252
262
  .option('--include-other-clients-session-context', 'include session context from other clients')
263
+ .option('--full-response', 'emit the full API response instead of compact default output when used with --json')
264
+ .option('--toon', 'emit compact TOON output for lower agent token use; formatting is applied in the CLI, not the API')
253
265
  .action(async (query, options, command) => {
254
266
  await runWithClient(command, async (client, json) => {
267
+ if (options.toon === true && (json || options.fullResponse === true)) {
268
+ throw new AppError(ErrorCode.VALIDATION, '--toon cannot be combined with --json or --full-response');
269
+ }
255
270
  if (options.memoryRole !== undefined &&
256
271
  !['durable_memory', 'session_context'].includes(options.memoryRole)) {
257
272
  throw new AppError(ErrorCode.VALIDATION, '--memory-role must be durable_memory or session_context');
@@ -270,7 +285,15 @@ program
270
285
  memory_role: options.memoryRole,
271
286
  include_other_clients_session_context: options.includeOtherClientsSessionContext === true ? true : undefined
272
287
  });
273
- return json ? body : formatSearchResults(body.results);
288
+ if (options.toon === true) {
289
+ return searchResponseToToon(compactSearchResponse(body));
290
+ }
291
+ if (json) {
292
+ return options.fullResponse === true
293
+ ? body
294
+ : compactSearchResponse(body);
295
+ }
296
+ return formatSearchResults(body.results);
274
297
  });
275
298
  });
276
299
  const memoryCommand = program
@@ -290,6 +313,7 @@ memoryCommand
290
313
  .option('--promotable', 'mark this session context as promotable')
291
314
  .option('--groom-after <groomAfter>', 'ISO timestamp after which grooming may archive/promote it')
292
315
  .option('--expires-at <expiresAt>', 'ISO timestamp after which the context is stale')
316
+ .option('--full-response', 'emit the full API response when used with --json')
293
317
  .action(async (content, options, command) => {
294
318
  await runWithClient(command, async (client, json) => {
295
319
  const body = await client.storeSessionContext({
@@ -305,7 +329,9 @@ memoryCommand
305
329
  expires_at: options.expiresAt
306
330
  });
307
331
  return json
308
- ? body
332
+ ? options.fullResponse === true
333
+ ? body
334
+ : compactStoredEntityResponse(body)
309
335
  : formatStoredEntity({
310
336
  id: body.entity.id,
311
337
  type: body.entity.type,
@@ -341,8 +367,11 @@ program
341
367
  .option('--limit <limit>', 'result limit', '50')
342
368
  .option('--offset <offset>', 'result offset', '0')
343
369
  .option('--include-archived', 'include archived entities')
370
+ .option('--full-response', 'emit the full API response instead of compact default output when used with --json')
371
+ .option('--toon', 'emit compact TOON output for lower agent token use; formatting is applied in the CLI, not the API')
344
372
  .action(async (options, command) => {
345
373
  await runWithClient(command, async (client, json) => {
374
+ validateToonOptions(options, json);
346
375
  const body = await client.listEntities({
347
376
  type: options.type,
348
377
  status: options.status,
@@ -353,8 +382,13 @@ program
353
382
  offset: Number(options.offset),
354
383
  include_archived: options.includeArchived === true ? true : undefined
355
384
  });
385
+ if (options.toon === true) {
386
+ return entityListResponseToToon(compactEntityListResponse(body));
387
+ }
356
388
  if (json) {
357
- return body;
389
+ return options.fullResponse === true
390
+ ? body
391
+ : compactEntityListResponse(body);
358
392
  }
359
393
  if (body.items.length === 0) {
360
394
  return ['No entities'];
@@ -387,6 +421,7 @@ program
387
421
  .option('--metadata <json>', 'JSON metadata object')
388
422
  .option('--version <version>', 'expected version')
389
423
  .option('--force', 'retry using the latest version on conflict')
424
+ .option('--full-response', 'emit the full API response when used with --json')
390
425
  .action(async (id, options, command) => {
391
426
  await runWithClient(command, async (client, json) => {
392
427
  const payload = {
@@ -412,7 +447,11 @@ program
412
447
  else {
413
448
  throw new Error('--version is required unless --force is set');
414
449
  }
415
- return json ? body : formatStoredEntity(body.entity);
450
+ return json
451
+ ? options.fullResponse === true
452
+ ? body
453
+ : compactStoredEntityResponse(body)
454
+ : formatStoredEntity(body.entity);
416
455
  });
417
456
  });
418
457
  program
@@ -436,6 +475,7 @@ taskCommand
436
475
  .option('--tags <tags>', 'comma-separated tags')
437
476
  .option('--visibility <visibility>', 'task visibility', 'shared')
438
477
  .option('--metadata <json>', 'JSON metadata object')
478
+ .option('--full-response', 'emit the full API response when used with --json')
439
479
  .action(async (content, options, command) => {
440
480
  await runWithClient(command, async (client, json) => {
441
481
  const taskContent = await resolveStoreContent(content);
@@ -449,7 +489,9 @@ taskCommand
449
489
  metadata: parseJsonObject(options.metadata)
450
490
  });
451
491
  return json
452
- ? body
492
+ ? options.fullResponse === true
493
+ ? body
494
+ : compactStoredEntityResponse(body)
453
495
  : formatStoredEntity({
454
496
  id: body.entity.id,
455
497
  type: body.entity.type,
@@ -469,8 +511,11 @@ taskCommand
469
511
  .option('--limit <limit>', 'result limit', '50')
470
512
  .option('--offset <offset>', 'result offset', '0')
471
513
  .option('--include-archived', 'include archived tasks')
514
+ .option('--full-response', 'emit the full API response instead of compact default output when used with --json')
515
+ .option('--toon', 'emit compact TOON output for lower agent token use; formatting is applied in the CLI, not the API')
472
516
  .action(async (options, command) => {
473
517
  await runWithClient(command, async (client, json) => {
518
+ validateToonOptions(options, json);
474
519
  const body = await client.listTasks({
475
520
  status: options.status,
476
521
  context: options.context,
@@ -478,7 +523,14 @@ taskCommand
478
523
  offset: Number(options.offset),
479
524
  include_archived: options.includeArchived === true ? true : undefined
480
525
  });
481
- return json ? body : formatTaskList(body.items);
526
+ if (options.toon === true) {
527
+ return entityListResponseToToon(compactEntityListResponse(body));
528
+ }
529
+ return json
530
+ ? options.fullResponse === true
531
+ ? body
532
+ : compactEntityListResponse(body)
533
+ : formatTaskList(body.items);
482
534
  });
483
535
  });
484
536
  taskCommand
@@ -493,6 +545,7 @@ taskCommand
493
545
  .option('--visibility <visibility>', 'updated task visibility')
494
546
  .option('--metadata <json>', 'JSON metadata object')
495
547
  .option('--version <version>', 'expected version', '')
548
+ .option('--full-response', 'emit the full API response when used with --json')
496
549
  .action(async (id, options, command) => {
497
550
  await runWithClient(command, async (client, json) => {
498
551
  if (!options.version) {
@@ -509,7 +562,9 @@ taskCommand
509
562
  metadata: parseJsonObject(options.metadata)
510
563
  });
511
564
  return json
512
- ? body
565
+ ? options.fullResponse === true
566
+ ? body
567
+ : compactStoredEntityResponse(body)
513
568
  : formatStoredEntity({
514
569
  id: body.entity.id,
515
570
  type: body.entity.type,
@@ -526,6 +581,7 @@ taskCommand
526
581
  .description('Mark a task complete')
527
582
  .argument('id', 'task ID')
528
583
  .option('--version <version>', 'expected version')
584
+ .option('--full-response', 'emit the full API response when used with --json')
529
585
  .action(async (id, options, command) => {
530
586
  await runWithClient(command, async (client, json) => {
531
587
  if (options.version === undefined) {
@@ -533,7 +589,9 @@ taskCommand
533
589
  }
534
590
  const body = await client.completeTask(id, Number(options.version));
535
591
  return json
536
- ? body
592
+ ? options.fullResponse === true
593
+ ? body
594
+ : compactStoredEntityResponse(body)
537
595
  : formatStoredEntity({
538
596
  id: body.entity.id,
539
597
  type: body.entity.type,
@@ -554,7 +612,9 @@ program
554
612
  if (json)
555
613
  return body;
556
614
  const e = body.embedding;
557
- const age = e.oldest_pending_secs !== null ? ` oldest_pending=${e.oldest_pending_secs}s` : '';
615
+ const age = e.oldest_pending_secs !== null
616
+ ? ` oldest_pending=${e.oldest_pending_secs}s`
617
+ : '';
558
618
  const lines = [
559
619
  `embedding: pending=${e.pending} completed=${e.completed} failed=${e.failed} retry_eligible=${e.retry_eligible}${age}`
560
620
  ];
@@ -603,8 +663,14 @@ program
603
663
  const manifest = await buildSyncManifest(resolvedDir);
604
664
  const config = await resolvePgmConfig();
605
665
  const client = createPgmClient(config);
606
- const manifestForServer = manifest.map(({ path: p, sha }) => ({ path: p, sha }));
607
- const diff = await client.diffSync({ repo: repoName, files: manifestForServer });
666
+ const manifestForServer = manifest.map(({ path: p, sha }) => ({
667
+ path: p,
668
+ sha
669
+ }));
670
+ const diff = await client.diffSync({
671
+ repo: repoName,
672
+ files: manifestForServer
673
+ });
608
674
  if (options.dryRun) {
609
675
  const newCount = diff.toUpload.filter((f) => f.reason === 'new').length;
610
676
  const changedCount = diff.toUpload.filter((f) => f.reason === 'changed').length;
@@ -650,7 +716,8 @@ program
650
716
  }
651
717
  const content = await fsReadFile(entry.fullPath, 'utf8');
652
718
  const size = Buffer.byteLength(content, 'utf8');
653
- if (batch.length > 0 && (batch.length >= BATCH_FILES || batchBytes + size > BATCH_BYTES)) {
719
+ if (batch.length > 0 &&
720
+ (batch.length >= BATCH_FILES || batchBytes + size > BATCH_BYTES)) {
654
721
  await flushBatch();
655
722
  }
656
723
  batch.push({ path: toUpload.path, sha: toUpload.sha, content });
@@ -687,6 +754,7 @@ program
687
754
  .argument('<target-id>', 'target entity ID')
688
755
  .requiredOption('--relation <relation>', 'relationship type')
689
756
  .option('--confidence <n>', 'confidence score 0-1', '1.0')
757
+ .option('--full-response', 'emit the full API response when used with --json')
690
758
  .action(async (sourceId, targetId, options, command) => {
691
759
  await runWithClient(command, async (client, json) => {
692
760
  const body = await client.createEdge({
@@ -695,9 +763,12 @@ program
695
763
  relation: options.relation,
696
764
  confidence: Number(options.confidence)
697
765
  });
698
- if (json)
699
- return body;
700
- return [`Linked ${shortId(sourceId)} → ${shortId(targetId)} (${options.relation})`];
766
+ if (json) {
767
+ return options.fullResponse === true ? body : compactEdgeResponse(body);
768
+ }
769
+ return [
770
+ `Linked ${shortId(sourceId)} → ${shortId(targetId)} (${options.relation})`
771
+ ];
701
772
  });
702
773
  });
703
774
  program
@@ -717,16 +788,25 @@ program
717
788
  .option('--depth <n>', 'traversal depth (1-3)', '1')
718
789
  .option('--relation <types>', 'comma-separated relation types')
719
790
  .option('--owner <owner>', 'owner filter')
791
+ .option('--full-response', 'emit the full API response instead of compact default output when used with --json')
792
+ .option('--toon', 'emit compact TOON output for lower agent token use; formatting is applied in the CLI, not the API')
720
793
  .action(async (entityId, options, command) => {
721
794
  await runWithClient(command, async (client, json) => {
795
+ validateToonOptions(options, json);
722
796
  const relationTypes = parseCommaList(options.relation);
723
797
  const body = await client.expandGraph(entityId, {
724
798
  depth: Number(options.depth),
725
799
  ...(relationTypes !== undefined ? { relationTypes } : {}),
726
800
  ...(options.owner !== undefined ? { owner: options.owner } : {})
727
801
  });
728
- if (json)
729
- return body;
802
+ if (options.toon === true) {
803
+ return graphResponseToToon(compactGraphResponse(body));
804
+ }
805
+ if (json) {
806
+ return options.fullResponse === true
807
+ ? body
808
+ : compactGraphResponse(body);
809
+ }
730
810
  const lines = [];
731
811
  lines.push(`Graph for ${shortId(entityId)}:`);
732
812
  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.29.0",
3
+ "version": "1.30.0",
4
4
  "description": "Postgram CLI — store, search, and manage entities from the command line",
5
5
  "type": "module",
6
6
  "bin": {