@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.
- package/dist/src/client.js +14 -2
- package/dist/src/pgm.js +21 -4
- package/dist/src/shared.js +13 -1
- package/package.json +3 -2
- package/dist/tests/unit/errors.test.js +0 -38
- package/dist/tests/unit/shared.test.js +0 -36
- package/dist/vitest.config.js +0 -7
package/dist/src/client.js
CHANGED
|
@@ -43,8 +43,13 @@ export function createPgmClient(options) {
|
|
|
43
43
|
body: input
|
|
44
44
|
});
|
|
45
45
|
},
|
|
46
|
-
recallEntity(id) {
|
|
47
|
-
|
|
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
|
-
.
|
|
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;
|
package/dist/src/shared.js
CHANGED
|
@@ -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
|
-
|
|
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": "
|
|
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
|
-
});
|