@ivotoby/postgram-cli 0.1.0 → 1.0.4

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.
@@ -43,8 +43,13 @@ export function createPgmClient(options) {
43
43
  body: input
44
44
  });
45
45
  },
46
- recallEntity(id) {
47
- return request(options, `/api/entities/${id}`);
46
+ recallEntity(id, input = {}) {
47
+ const params = new URLSearchParams();
48
+ if (input.owner) {
49
+ params.set('owner', input.owner);
50
+ }
51
+ const query = params.toString();
52
+ return request(options, `/api/entities/${id}${query ? `?${query}` : ''}`);
48
53
  },
49
54
  searchEntities(input) {
50
55
  return request(options, '/api/search', {
@@ -74,6 +79,9 @@ export function createPgmClient(options) {
74
79
  if (input.visibility) {
75
80
  params.set('visibility', input.visibility);
76
81
  }
82
+ if (input.owner) {
83
+ params.set('owner', input.owner);
84
+ }
77
85
  if (input.tags?.length) {
78
86
  params.set('tags', input.tags.join(','));
79
87
  }
@@ -146,6 +154,8 @@ export function createPgmClient(options) {
146
154
  qs.set('relation', params.relation);
147
155
  if (params.direction)
148
156
  qs.set('direction', params.direction);
157
+ if (params.owner)
158
+ qs.set('owner', params.owner);
149
159
  const query = qs.toString();
150
160
  return request(options, `/api/entities/${entityId}/edges${query ? `?${query}` : ''}`);
151
161
  },
@@ -155,6 +165,8 @@ export function createPgmClient(options) {
155
165
  qs.set('depth', String(params.depth));
156
166
  if (params.relationTypes?.length)
157
167
  qs.set('relation_types', params.relationTypes.join(','));
168
+ if (params.owner)
169
+ qs.set('owner', params.owner);
158
170
  const query = qs.toString();
159
171
  return request(options, `/api/entities/${entityId}/graph${query ? `?${query}` : ''}`);
160
172
  }
package/dist/src/pgm.js CHANGED
@@ -13,6 +13,7 @@ function formatStoredEntity(entity) {
13
13
  return [
14
14
  `${entity.type} ${shortId(entity.id)}${entity.status ? ` [${entity.status}]` : ''}`,
15
15
  `visibility: ${entity.visibility}`,
16
+ `owner: ${entity.owner ?? 'shared'}`,
16
17
  `tags: ${entity.tags.join(', ') || '-'}`,
17
18
  entity.content ? `content: ${entity.content}` : 'content: -'
18
19
  ];
@@ -190,10 +191,12 @@ program
190
191
  .option('--json', 'emit JSON output');
191
192
  program
192
193
  .command('store')
194
+ .alias('add')
193
195
  .description('Store an entity')
194
196
  .argument('[content]', 'entity content')
195
197
  .option('--type <type>', 'entity type', 'memory')
196
198
  .option('--visibility <visibility>', 'entity visibility', 'shared')
199
+ .option('--owner <owner>', 'entity owner or namespace')
197
200
  .option('--status <status>', 'entity status')
198
201
  .option('--tags <tags>', 'comma-separated tags')
199
202
  .option('--source <source>', 'entity source')
@@ -204,6 +207,7 @@ program
204
207
  type: options.type,
205
208
  content: await resolveStoreContent(content),
206
209
  visibility: options.visibility,
210
+ owner: options.owner,
207
211
  status: options.status,
208
212
  tags: parseCommaList(options.tags),
209
213
  source: options.source,
@@ -217,6 +221,7 @@ program
217
221
  content: body.entity.content,
218
222
  status: body.entity.status,
219
223
  visibility: body.entity.visibility,
224
+ owner: body.entity.owner,
220
225
  tags: body.entity.tags
221
226
  });
222
227
  });
@@ -228,6 +233,7 @@ program
228
233
  .option('--type <type>', 'entity type')
229
234
  .option('--tags <tags>', 'comma-separated tags')
230
235
  .option('--visibility <visibility>', 'entity visibility filter')
236
+ .option('--owner <owner>', 'entity owner filter')
231
237
  .option('--limit <limit>', 'result limit', '10')
232
238
  .option('--threshold <threshold>', 'similarity threshold', '0.35')
233
239
  .option('--recency-weight <recencyWeight>', 'recency weight', '0.1')
@@ -238,6 +244,7 @@ program
238
244
  type: options.type,
239
245
  tags: parseCommaList(options.tags),
240
246
  visibility: options.visibility,
247
+ owner: options.owner,
241
248
  limit: Number(options.limit),
242
249
  threshold: Number(options.threshold),
243
250
  recency_weight: Number(options.recencyWeight)
@@ -249,9 +256,12 @@ program
249
256
  .command('recall')
250
257
  .description('Recall an entity by ID')
251
258
  .argument('id', 'entity ID')
252
- .action(async (id, _options, command) => {
259
+ .option('--owner <owner>', 'entity owner filter')
260
+ .action(async (id, options, command) => {
253
261
  await runWithClient(command, async (client, json) => {
254
- const body = await client.recallEntity(id);
262
+ const body = await client.recallEntity(id, {
263
+ owner: options.owner
264
+ });
255
265
  return json ? body : formatStoredEntity(body.entity);
256
266
  });
257
267
  });
@@ -261,6 +271,7 @@ program
261
271
  .option('--type <type>', 'filter by type')
262
272
  .option('--status <status>', 'filter by status')
263
273
  .option('--visibility <visibility>', 'filter by visibility')
274
+ .option('--owner <owner>', 'filter by owner')
264
275
  .option('--tags <tags>', 'comma-separated tags')
265
276
  .option('--limit <limit>', 'result limit', '50')
266
277
  .option('--offset <offset>', 'result offset', '0')
@@ -270,6 +281,7 @@ program
270
281
  type: options.type,
271
282
  status: options.status,
272
283
  visibility: options.visibility,
284
+ owner: options.owner,
273
285
  tags: parseCommaList(options.tags),
274
286
  limit: Number(options.limit),
275
287
  offset: Number(options.offset)
@@ -288,7 +300,7 @@ program
288
300
  : '-';
289
301
  return [
290
302
  `${item.type} ${shortId(item.id)} ${preview}`,
291
- ` tags: ${item.tags.join(', ') || '-'} | ${item.visibility} | ${item.created_at.slice(0, 10)}`
303
+ ` tags: ${item.tags.join(', ') || '-'} | owner=${item.owner ?? 'shared'} | ${item.visibility} | ${item.created_at.slice(0, 10)}`
292
304
  ];
293
305
  });
294
306
  lines.push('');
@@ -377,6 +389,7 @@ taskCommand
377
389
  content: body.entity.content,
378
390
  status: body.entity.status,
379
391
  visibility: body.entity.visibility,
392
+ owner: body.entity.owner,
380
393
  tags: body.entity.tags
381
394
  });
382
395
  });
@@ -434,6 +447,7 @@ taskCommand
434
447
  content: body.entity.content,
435
448
  status: body.entity.status,
436
449
  visibility: body.entity.visibility,
450
+ owner: body.entity.owner,
437
451
  tags: body.entity.tags
438
452
  });
439
453
  });
@@ -457,6 +471,7 @@ taskCommand
457
471
  content: body.entity.content,
458
472
  status: body.entity.status,
459
473
  visibility: body.entity.visibility,
474
+ owner: body.entity.owner,
460
475
  tags: body.entity.tags
461
476
  });
462
477
  });
@@ -608,12 +623,14 @@ program
608
623
  .argument('<entity-id>', 'entity ID')
609
624
  .option('--depth <n>', 'traversal depth (1-3)', '1')
610
625
  .option('--relation <types>', 'comma-separated relation types')
626
+ .option('--owner <owner>', 'owner filter')
611
627
  .action(async (entityId, options, command) => {
612
628
  await runWithClient(command, async (client, json) => {
613
629
  const relationTypes = parseCommaList(options.relation);
614
630
  const body = await client.expandGraph(entityId, {
615
631
  depth: Number(options.depth),
616
- ...(relationTypes !== undefined ? { relationTypes } : {})
632
+ ...(relationTypes !== undefined ? { relationTypes } : {}),
633
+ ...(options.owner !== undefined ? { owner: options.owner } : {})
617
634
  });
618
635
  if (json)
619
636
  return body;
@@ -1,3 +1,4 @@
1
+ import { statSync } from 'node:fs';
1
2
  import { readFile } from 'node:fs/promises';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
@@ -19,7 +20,13 @@ export function parseJsonObject(value, fallback = {}) {
19
20
  if (value === undefined) {
20
21
  return fallback;
21
22
  }
22
- const parsed = JSON.parse(value);
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(value);
26
+ }
27
+ catch (error) {
28
+ throw new AppError(ErrorCode.VALIDATION, `Invalid JSON: ${error instanceof Error ? error.message : 'parse failed'}`);
29
+ }
23
30
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
24
31
  throw new AppError(ErrorCode.VALIDATION, 'Metadata must be a JSON object');
25
32
  }
@@ -39,6 +46,7 @@ export async function readStdinText() {
39
46
  process.stdin.on('error', reject);
40
47
  });
41
48
  }
49
+ const INSECURE_PGMRC_MODE_MASK = 0o077;
42
50
  function normalizeUrl(url) {
43
51
  return url.replace(/\/+$/, '');
44
52
  }
@@ -54,6 +62,10 @@ export async function resolvePgmConfig() {
54
62
  const rcPath = path.join(os.homedir(), '.pgmrc');
55
63
  try {
56
64
  const raw = await readFile(rcPath, 'utf8');
65
+ const fileStat = statSync(rcPath);
66
+ if ((fileStat.mode & INSECURE_PGMRC_MODE_MASK) !== 0) {
67
+ process.stderr.write("Warning: ~/.pgmrc is accessible by other users. Run 'chmod 600 ~/.pgmrc' to fix.\n");
68
+ }
57
69
  const parsed = JSON.parse(raw);
58
70
  if (!parsed.api_url || !parsed.api_key) {
59
71
  throw new Error('missing api_url or api_key');
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@ivotoby/postgram-cli",
3
- "version": "0.1.0",
3
+ "version": "1.0.4",
4
4
  "description": "Postgram CLI — store, search, and manage entities from the command line",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "pgm": "dist/src/pgm.js"
8
8
  },
9
9
  "files": [
10
- "dist",
10
+ "dist/src",
11
11
  "README.md"
12
12
  ],
13
13
  "repository": {
@@ -45,6 +45,7 @@
45
45
  "@eslint/js": "^9.38.0",
46
46
  "@semantic-release/changelog": "^6.0.3",
47
47
  "@semantic-release/commit-analyzer": "^13.0.1",
48
+ "@semantic-release/exec": "^7.1.0",
48
49
  "@semantic-release/git": "^10.0.1",
49
50
  "@semantic-release/github": "^11.0.6",
50
51
  "@semantic-release/npm": "^12.0.2",
@@ -1,38 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { AppError, ErrorCode, toErrorResponse } from '../../src/errors.js';
3
- describe('AppError', () => {
4
- it('creates an error with code and message', () => {
5
- const error = new AppError(ErrorCode.VALIDATION, 'test message');
6
- expect(error.code).toBe(ErrorCode.VALIDATION);
7
- expect(error.message).toBe('test message');
8
- expect(error.details).toEqual({});
9
- });
10
- it('creates an error with details', () => {
11
- const error = new AppError(ErrorCode.INTERNAL, 'fail', { key: 'value' });
12
- expect(error.details).toEqual({ key: 'value' });
13
- });
14
- });
15
- describe('toErrorResponse', () => {
16
- it('formats error response', () => {
17
- const error = new AppError(ErrorCode.NOT_FOUND, 'missing', { id: 'abc' });
18
- const response = toErrorResponse(error);
19
- expect(response).toEqual({
20
- error: {
21
- code: 'NOT_FOUND',
22
- message: 'missing',
23
- details: { id: 'abc' }
24
- }
25
- });
26
- });
27
- });
28
- describe('ErrorCode', () => {
29
- it('contains all expected codes', () => {
30
- expect(ErrorCode.VALIDATION).toBe('VALIDATION');
31
- expect(ErrorCode.UNAUTHORIZED).toBe('UNAUTHORIZED');
32
- expect(ErrorCode.FORBIDDEN).toBe('FORBIDDEN');
33
- expect(ErrorCode.NOT_FOUND).toBe('NOT_FOUND');
34
- expect(ErrorCode.CONFLICT).toBe('CONFLICT');
35
- expect(ErrorCode.EMBEDDING_FAILED).toBe('EMBEDDING_FAILED');
36
- expect(ErrorCode.INTERNAL).toBe('INTERNAL');
37
- });
38
- });
@@ -1,36 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { parseCommaList, parseJsonObject, shortId } from '../../src/shared.js';
3
- import { AppError, ErrorCode } from '../../src/errors.js';
4
- describe('parseCommaList', () => {
5
- it('returns undefined for undefined input', () => {
6
- expect(parseCommaList(undefined)).toBeUndefined();
7
- });
8
- it('splits comma-separated values and trims whitespace', () => {
9
- expect(parseCommaList('a, b, c')).toEqual(['a', 'b', 'c']);
10
- });
11
- it('filters empty strings', () => {
12
- expect(parseCommaList('a,,b')).toEqual(['a', 'b']);
13
- });
14
- it('returns undefined for all-empty result', () => {
15
- expect(parseCommaList(' , , ')).toBeUndefined();
16
- });
17
- });
18
- describe('parseJsonObject', () => {
19
- it('returns fallback for undefined', () => {
20
- expect(parseJsonObject(undefined)).toEqual({});
21
- });
22
- it('parses valid JSON object', () => {
23
- expect(parseJsonObject('{"key":"value"}')).toEqual({ key: 'value' });
24
- });
25
- it('throws for non-object JSON', () => {
26
- expect(() => parseJsonObject('[1,2]')).toThrow(AppError);
27
- });
28
- it('throws for invalid JSON', () => {
29
- expect(() => parseJsonObject('not json')).toThrow();
30
- });
31
- });
32
- describe('shortId', () => {
33
- it('returns first 8 characters', () => {
34
- expect(shortId('abcdefgh-1234-5678')).toBe('abcdefgh');
35
- });
36
- });
@@ -1,7 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
- export default defineConfig({
3
- test: {
4
- environment: 'node',
5
- include: ['tests/**/*.test.ts']
6
- }
7
- });