@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 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 && 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,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
- ? body
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('--include-other-clients-session-context', 'include session context from other clients')
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
- return json ? body : formatSearchResults(body.results);
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
- ? body
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 body;
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 ? body : formatStoredEntity(body.entity);
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
- ? body
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
- return json ? body : formatTaskList(body.items);
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
- ? body
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
- ? body
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 ? ` oldest_pending=${e.oldest_pending_secs}s` : '';
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 }) => ({ path: p, sha }));
607
- const diff = await client.diffSync({ repo: repoName, files: manifestForServer });
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 && (batch.length >= BATCH_FILES || batchBytes + size > BATCH_BYTES)) {
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
- return [`Linked ${shortId(sourceId)} → ${shortId(targetId)} (${options.relation})`];
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 (json)
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.29.0",
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",