@pyxmate/memory 0.19.1 → 0.20.5
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/bin/init.mjs +633 -24
- package/dist/index.d.ts +5 -2
- package/dist/index.mjs +6 -3
- package/package.json +1 -1
- package/skills/pyx-memory/SKILL.md +8 -23
package/bin/init.mjs
CHANGED
|
@@ -1,43 +1,312 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { cpSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
4
|
-
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const DEFAULT_ENDPOINT = 'https://memory.api.pyxmate.com';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
// ---------- Argument parsing ----------
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Minimal long-flag parser. Supports `--key=value`, `--key value`, boolean `--flag`,
|
|
14
|
+
* `--` terminator, and positional rest. Unknown subcommand-specific flags surface
|
|
15
|
+
* as keys in `flags`; the dispatcher decides if any are unsupported.
|
|
16
|
+
*/
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
const flags = Object.create(null);
|
|
19
|
+
const positional = [];
|
|
20
|
+
let i = 0;
|
|
21
|
+
while (i < argv.length) {
|
|
22
|
+
const a = argv[i];
|
|
23
|
+
if (a === '--') {
|
|
24
|
+
positional.push(...argv.slice(i + 1));
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
if (a.startsWith('--')) {
|
|
28
|
+
const eq = a.indexOf('=');
|
|
29
|
+
if (eq >= 0) {
|
|
30
|
+
const key = a.slice(2, eq);
|
|
31
|
+
flags[key] = a.slice(eq + 1);
|
|
32
|
+
i += 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const key = a.slice(2);
|
|
36
|
+
const next = argv[i + 1];
|
|
37
|
+
if (next === undefined || next.startsWith('--')) {
|
|
38
|
+
flags[key] = true;
|
|
39
|
+
i += 1;
|
|
40
|
+
} else {
|
|
41
|
+
flags[key] = next;
|
|
42
|
+
i += 2;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
positional.push(a);
|
|
47
|
+
i += 1;
|
|
48
|
+
}
|
|
49
|
+
return { flags, positional };
|
|
50
|
+
}
|
|
13
51
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
52
|
+
// ---------- Errors ----------
|
|
53
|
+
|
|
54
|
+
class CliError extends Error {
|
|
55
|
+
constructor(message, exitCode = 1) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.exitCode = exitCode;
|
|
58
|
+
}
|
|
17
59
|
}
|
|
18
60
|
|
|
19
|
-
|
|
20
|
-
const target = resolve(process.cwd(), '.claude', 'skills', 'pyx-memory');
|
|
61
|
+
// ---------- Shared helpers ----------
|
|
21
62
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
63
|
+
function resolveAuth(flags) {
|
|
64
|
+
const endpointRaw = flags.endpoint ?? process.env.PYXMATE_ENDPOINT ?? DEFAULT_ENDPOINT;
|
|
65
|
+
if (typeof endpointRaw !== 'string') {
|
|
66
|
+
throw new CliError('--endpoint requires a value (got bare flag)', 2);
|
|
67
|
+
}
|
|
68
|
+
const endpoint = endpointRaw.replace(/\/+$/, '');
|
|
69
|
+
const apiKey = flags['api-key'] ?? process.env.PYXMATE_API_KEY;
|
|
70
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
71
|
+
throw new CliError('missing API key: set PYXMATE_API_KEY or pass --api-key <key>', 2);
|
|
72
|
+
}
|
|
73
|
+
return { apiKey, endpoint };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildHeaders(flags, { apiKey, json = false, includeSensitivity = false } = {}) {
|
|
77
|
+
const headers = { Authorization: `Bearer ${apiKey}` };
|
|
78
|
+
if (json) headers['Content-Type'] = 'application/json';
|
|
79
|
+
// Bare `--tenant-id`/`--user-id`/`--team-id` (no value) becomes `true` via parseArgs;
|
|
80
|
+
// `String(true)` would silently scope the request to a tenant literally named "true".
|
|
81
|
+
// Type-guard mirrors resolveAuth's `--endpoint` shape.
|
|
82
|
+
for (const [flag, header] of [
|
|
83
|
+
['tenant-id', 'X-Tenant-Id'],
|
|
84
|
+
['user-id', 'X-User-Id'],
|
|
85
|
+
['team-id', 'X-Team-Id'],
|
|
86
|
+
]) {
|
|
87
|
+
if (flags[flag] !== undefined) {
|
|
88
|
+
if (typeof flags[flag] !== 'string') {
|
|
89
|
+
throw new CliError(`--${flag} requires a value (got bare flag)`, 2);
|
|
90
|
+
}
|
|
91
|
+
headers[header] = flags[flag];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (includeSensitivity && flags.sensitivity) {
|
|
95
|
+
const level = String(flags.sensitivity);
|
|
96
|
+
if (!['public', 'internal', 'secret'].includes(level)) {
|
|
97
|
+
throw new CliError(
|
|
98
|
+
`--sensitivity must be one of: public, internal, secret (got '${level}')`,
|
|
99
|
+
2,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
headers['X-Caller-Access-Level'] = level;
|
|
103
|
+
}
|
|
104
|
+
return headers;
|
|
25
105
|
}
|
|
26
106
|
|
|
27
|
-
|
|
28
|
-
|
|
107
|
+
function loadJsonArg(value, flagName) {
|
|
108
|
+
if (value === undefined || value === null || value === true) {
|
|
109
|
+
throw new CliError(`--${flagName} requires a value`, 2);
|
|
110
|
+
}
|
|
111
|
+
const raw = String(value);
|
|
112
|
+
let source;
|
|
113
|
+
let payload;
|
|
114
|
+
try {
|
|
115
|
+
if (raw === '-') {
|
|
116
|
+
source = 'stdin';
|
|
117
|
+
// Synchronous stdin read for ergonomics; only invoked when caller explicitly passes '-'.
|
|
118
|
+
payload = readFileSync(0, 'utf8');
|
|
119
|
+
} else if (raw.startsWith('@')) {
|
|
120
|
+
source = `file ${raw.slice(1)}`;
|
|
121
|
+
payload = readFileSync(raw.slice(1), 'utf8');
|
|
122
|
+
} else {
|
|
123
|
+
source = 'inline';
|
|
124
|
+
payload = raw;
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
throw new CliError(`--${flagName}: failed to read ${source}: ${err.message}`, 2);
|
|
128
|
+
}
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(payload);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
throw new CliError(`--${flagName}: invalid JSON from ${source}: ${err.message}`, 2);
|
|
134
|
+
}
|
|
135
|
+
if (!Array.isArray(parsed)) {
|
|
136
|
+
throw new CliError(`--${flagName}: expected JSON array, got ${typeof parsed}`, 2);
|
|
137
|
+
}
|
|
138
|
+
return parsed;
|
|
139
|
+
}
|
|
29
140
|
|
|
30
|
-
|
|
31
|
-
|
|
141
|
+
async function request(method, url, { headers = {}, body = undefined } = {}) {
|
|
142
|
+
let response;
|
|
143
|
+
try {
|
|
144
|
+
response = await fetch(url, { method, headers, body });
|
|
145
|
+
} catch (err) {
|
|
146
|
+
throw new CliError(`network error: ${err.message}`, 1);
|
|
147
|
+
}
|
|
148
|
+
const text = await response.text();
|
|
149
|
+
let parsed;
|
|
150
|
+
if (text.length === 0) {
|
|
151
|
+
parsed = null;
|
|
152
|
+
} else {
|
|
153
|
+
try {
|
|
154
|
+
parsed = JSON.parse(text);
|
|
155
|
+
} catch {
|
|
156
|
+
// Server returned non-JSON; surface raw text so the failure is visible.
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
throw new CliError(`HTTP ${response.status}: ${text.slice(0, 500)}`, 1);
|
|
159
|
+
}
|
|
160
|
+
throw new CliError(`expected JSON response from ${url}; got: ${text.slice(0, 200)}`, 1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
const msg =
|
|
165
|
+
parsed && (parsed.error || parsed.message)
|
|
166
|
+
? parsed.error || parsed.message
|
|
167
|
+
: `HTTP ${response.status}`;
|
|
168
|
+
throw new CliError(`server error (${response.status}): ${msg}`, 1);
|
|
169
|
+
}
|
|
170
|
+
// Surface `{ success: false }` envelope errors.
|
|
171
|
+
if (parsed && parsed.success === false) {
|
|
172
|
+
const msg = parsed.error || parsed.message || 'unknown error';
|
|
173
|
+
throw new CliError(`server error: ${msg}`, 1);
|
|
174
|
+
}
|
|
175
|
+
return parsed;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function emit(result, flags, humanFn) {
|
|
179
|
+
if (flags.json) {
|
|
180
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const human = humanFn ? humanFn(result) : JSON.stringify(result);
|
|
184
|
+
if (human !== undefined && human !== null && human !== '') {
|
|
185
|
+
process.stdout.write(`${human}\n`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------- Help text ----------
|
|
190
|
+
|
|
191
|
+
const TOP_HELP = `pyxmate-memory — CLI for the pyx-memory HTTP API
|
|
192
|
+
|
|
193
|
+
Usage:
|
|
194
|
+
pyxmate-memory <command> [flags]
|
|
195
|
+
|
|
196
|
+
Commands:
|
|
197
|
+
init Copy pyx-memory skill files into ./.claude/skills/
|
|
198
|
+
store Store a memory entry (POST /api/memory/ingest)
|
|
199
|
+
search Search memory (GET /api/memory/search)
|
|
200
|
+
get <id> Fetch a single entry (GET /api/memory/:id)
|
|
201
|
+
delete <id> Delete an entry (DELETE /api/memory/:id)
|
|
202
|
+
log List recent entries (GET /api/memory/log)
|
|
203
|
+
summarize-entity <name> Summarize an entity (POST /api/memory/summarize-entity)
|
|
204
|
+
ingest-file <path> Upload a file (POST /api/memory/ingest/file, multipart NDJSON)
|
|
205
|
+
|
|
206
|
+
Auth (operation commands):
|
|
207
|
+
PYXMATE_API_KEY=<key> API key (or --api-key <key>)
|
|
208
|
+
PYXMATE_ENDPOINT=<url> Endpoint (or --endpoint <url>, default ${DEFAULT_ENDPOINT})
|
|
209
|
+
|
|
210
|
+
Global flags (operation commands):
|
|
211
|
+
--tenant-id <id> Forward X-Tenant-Id header
|
|
212
|
+
--user-id <id> Forward X-User-Id header
|
|
213
|
+
--team-id <id> Forward X-Team-Id header
|
|
214
|
+
--json Emit raw upstream JSON to stdout
|
|
215
|
+
--help Print help for the command
|
|
216
|
+
|
|
217
|
+
Run \`pyxmate-memory <command> --help\` for command-specific flags.`;
|
|
218
|
+
|
|
219
|
+
const SUB_HELP = {
|
|
220
|
+
init: `Usage: pyxmate-memory init
|
|
221
|
+
|
|
222
|
+
Copies skill files into ./.claude/skills/pyx-memory/ for Claude Code auto-discovery.
|
|
223
|
+
Requires no API key; idempotent overwrite.`,
|
|
224
|
+
store: `Usage: pyxmate-memory store --content <text> [flags]
|
|
225
|
+
|
|
226
|
+
--content <text> Memory content (required)
|
|
227
|
+
--type <type> Memory type (default: long-term)
|
|
228
|
+
--topic <topic> Topic (metadata.topic)
|
|
229
|
+
--project <name> Project (metadata.project)
|
|
230
|
+
--importance <1-10> Importance score (metadata.importance)
|
|
231
|
+
--source <source> Source (metadata.source)
|
|
232
|
+
--event-time <iso> Event time (metadata.eventTime)
|
|
233
|
+
--entities <json|@path|-> JSON array of entities (inline, @file, or - for stdin)
|
|
234
|
+
--relationships <json|@path|-> JSON array of relationships (inline, @file, or - for stdin)
|
|
235
|
+
--quiet Print only the new entry id`,
|
|
236
|
+
search: `Usage: pyxmate-memory search --query <text> [flags]
|
|
237
|
+
|
|
238
|
+
--query <text> Search query (required; also accepts --q)
|
|
239
|
+
--limit <n> Max results (default 5)
|
|
240
|
+
--strategy <s> hybrid | vector | keyword | graph
|
|
241
|
+
--type <t> Filter by memory type
|
|
242
|
+
--agent-id <id> Filter by agentId
|
|
243
|
+
--abstention-threshold <0-1> Enable confidence scoring
|
|
244
|
+
--sensitivity <level> X-Caller-Access-Level (public|internal|secret)`,
|
|
245
|
+
get: `Usage: pyxmate-memory get <id> [flags]`,
|
|
246
|
+
delete: `Usage: pyxmate-memory delete <id> [flags]`,
|
|
247
|
+
log: `Usage: pyxmate-memory log [flags]
|
|
248
|
+
|
|
249
|
+
--since <iso> Only entries created at or after this ISO timestamp
|
|
250
|
+
--limit <n> Max entries
|
|
251
|
+
--type <t> Filter by memory type
|
|
252
|
+
--agent-id <id> Filter by agentId
|
|
253
|
+
--source <s> Filter by source`,
|
|
254
|
+
'summarize-entity': `Usage: pyxmate-memory summarize-entity <name> [flags]`,
|
|
255
|
+
'ingest-file': `Usage: pyxmate-memory ingest-file <path> [flags]
|
|
256
|
+
|
|
257
|
+
--description <text> Required for images; describes the file contents
|
|
258
|
+
--namespace-id <id> Namespace for the entry`,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const GLOBAL_FLAGS_FOOTER = `
|
|
262
|
+
Global flags:
|
|
263
|
+
--api-key <key> API key (or PYXMATE_API_KEY)
|
|
264
|
+
--endpoint <url> Endpoint URL (or PYXMATE_ENDPOINT)
|
|
265
|
+
--tenant-id <id> Forward X-Tenant-Id header
|
|
266
|
+
--user-id <id> Forward X-User-Id header
|
|
267
|
+
--team-id <id> Forward X-Team-Id header
|
|
268
|
+
--json Emit raw upstream JSON to stdout`;
|
|
269
|
+
|
|
270
|
+
function printHelp(subcommand) {
|
|
271
|
+
if (subcommand && SUB_HELP[subcommand]) {
|
|
272
|
+
const footer = subcommand === 'init' ? '' : `\n${GLOBAL_FLAGS_FOOTER}`;
|
|
273
|
+
process.stdout.write(`${SUB_HELP[subcommand]}${footer}\n`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
process.stdout.write(`${TOP_HELP}\n`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------- init (existing behavior) ----------
|
|
280
|
+
|
|
281
|
+
function runInit() {
|
|
282
|
+
const source = resolve(__dirname, '..', 'skills', 'pyx-memory');
|
|
283
|
+
const target = resolve(process.cwd(), '.claude', 'skills', 'pyx-memory');
|
|
284
|
+
|
|
285
|
+
if (!existsSync(source)) {
|
|
286
|
+
throw new CliError(
|
|
287
|
+
'Skills directory not found in package. Please reinstall @pyxmate/memory.',
|
|
288
|
+
1,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
293
|
+
cpSync(source, target, { recursive: true });
|
|
294
|
+
|
|
295
|
+
const fileCount = countFiles(target);
|
|
296
|
+
const fileList = listFiles(target);
|
|
297
|
+
|
|
298
|
+
process.stdout.write(`Copied ${fileCount} skill files to .claude/skills/pyx-memory/\n\n`);
|
|
299
|
+
process.stdout.write(`${fileList.join('\n')}\n`);
|
|
300
|
+
process.stdout.write('\nClaude Code will now auto-discover pyx-memory integration patterns.\n');
|
|
301
|
+
}
|
|
32
302
|
|
|
33
|
-
// Count copied files
|
|
34
303
|
function countFiles(dir) {
|
|
35
304
|
let count = 0;
|
|
36
305
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
37
306
|
if (entry.isDirectory()) {
|
|
38
307
|
count += countFiles(join(dir, entry.name));
|
|
39
308
|
} else {
|
|
40
|
-
count
|
|
309
|
+
count += 1;
|
|
41
310
|
}
|
|
42
311
|
}
|
|
43
312
|
return count;
|
|
@@ -55,9 +324,349 @@ function listFiles(dir, prefix = '') {
|
|
|
55
324
|
return lines;
|
|
56
325
|
}
|
|
57
326
|
|
|
58
|
-
|
|
59
|
-
|
|
327
|
+
// ---------- Operation subcommands ----------
|
|
328
|
+
|
|
329
|
+
async function runStore(flags) {
|
|
330
|
+
if (!flags.content || flags.content === true) {
|
|
331
|
+
throw new CliError('--content is required', 2);
|
|
332
|
+
}
|
|
333
|
+
const { apiKey, endpoint } = resolveAuth(flags);
|
|
334
|
+
const headers = buildHeaders(flags, { apiKey, json: true });
|
|
335
|
+
|
|
336
|
+
const metadata = {};
|
|
337
|
+
if (flags.topic) metadata.topic = String(flags.topic);
|
|
338
|
+
if (flags.project) metadata.project = String(flags.project);
|
|
339
|
+
|
|
340
|
+
const body = {
|
|
341
|
+
content: String(flags.content),
|
|
342
|
+
type: flags.type ? String(flags.type) : 'long-term',
|
|
343
|
+
};
|
|
344
|
+
if (Object.keys(metadata).length > 0) body.metadata = metadata;
|
|
345
|
+
if (flags.source) body.source = String(flags.source);
|
|
346
|
+
if (flags.importance !== undefined) {
|
|
347
|
+
const n = Number(flags.importance);
|
|
348
|
+
if (!Number.isInteger(n) || n < 1 || n > 10) {
|
|
349
|
+
throw new CliError(
|
|
350
|
+
`--importance must be an integer in [1,10] (got '${flags.importance}')`,
|
|
351
|
+
2,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
body.importance = n;
|
|
355
|
+
}
|
|
356
|
+
if (flags['event-time']) {
|
|
357
|
+
const iso = String(flags['event-time']);
|
|
358
|
+
if (
|
|
359
|
+
!/^\d{4}-\d{2}-\d{2}T/.test(iso) ||
|
|
360
|
+
!/(Z|[+-]\d{2}:?\d{2})$/.test(iso) ||
|
|
361
|
+
!Number.isFinite(new Date(iso).getTime())
|
|
362
|
+
) {
|
|
363
|
+
throw new CliError(
|
|
364
|
+
`--event-time must be ISO 8601 (e.g. 2026-04-15T10:00:00Z); got '${iso}'`,
|
|
365
|
+
2,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
body.eventTime = iso;
|
|
369
|
+
}
|
|
370
|
+
if (flags.entities !== undefined) body.entities = loadJsonArg(flags.entities, 'entities');
|
|
371
|
+
if (flags.relationships !== undefined)
|
|
372
|
+
body.relationships = loadJsonArg(flags.relationships, 'relationships');
|
|
373
|
+
|
|
374
|
+
const result = await request('POST', `${endpoint}/api/memory/ingest`, {
|
|
375
|
+
headers,
|
|
376
|
+
body: JSON.stringify(body),
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
if (flags.quiet) {
|
|
380
|
+
const id = (result && result.data && result.data.id) || (result && result.id);
|
|
381
|
+
if (!id) throw new CliError('--quiet: server response did not contain an id', 1);
|
|
382
|
+
process.stdout.write(`${id}\n`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
emit(result, flags, (r) => {
|
|
386
|
+
const id = (r && r.data && r.data.id) || (r && r.id) || '<no id>';
|
|
387
|
+
return `stored ${id}`;
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function runSearch(flags) {
|
|
392
|
+
const query = flags.query ?? flags.q;
|
|
393
|
+
if (!query || query === true) throw new CliError('--query is required', 2);
|
|
394
|
+
const { apiKey, endpoint } = resolveAuth(flags);
|
|
395
|
+
const headers = buildHeaders(flags, { apiKey, includeSensitivity: true });
|
|
396
|
+
|
|
397
|
+
const params = new URLSearchParams();
|
|
398
|
+
// Cloud reads `q`; self-hosted reads `query`. Send both for runtime compat.
|
|
399
|
+
params.set('query', String(query));
|
|
400
|
+
params.set('q', String(query));
|
|
401
|
+
if (flags.limit !== undefined) params.set('limit', String(flags.limit));
|
|
402
|
+
if (flags.strategy) params.set('strategy', String(flags.strategy));
|
|
403
|
+
if (flags.type) params.set('type', String(flags.type));
|
|
404
|
+
if (flags['agent-id']) params.set('agentId', String(flags['agent-id']));
|
|
405
|
+
if (flags['abstention-threshold'] !== undefined) {
|
|
406
|
+
params.set('abstentionThreshold', String(flags['abstention-threshold']));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const result = await request('GET', `${endpoint}/api/memory/search?${params.toString()}`, {
|
|
410
|
+
headers,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
emit(result, flags, (r) => {
|
|
414
|
+
const results = (r && r.data && r.data.results) || (r && r.results) || [];
|
|
415
|
+
if (!Array.isArray(results) || results.length === 0) return 'no results';
|
|
416
|
+
return results
|
|
417
|
+
.map((m, idx) => {
|
|
418
|
+
const id = m.id ?? '<no id>';
|
|
419
|
+
const content = (m.content ?? '').slice(0, 100);
|
|
420
|
+
const score = typeof m.score === 'number' ? ` [score=${m.score.toFixed(3)}]` : '';
|
|
421
|
+
return `${idx + 1}. ${id}${score} ${content}`;
|
|
422
|
+
})
|
|
423
|
+
.join('\n');
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function runGet(flags, positional) {
|
|
428
|
+
const id = positional[0];
|
|
429
|
+
if (!id) throw new CliError('get: <id> argument is required', 2);
|
|
430
|
+
const { apiKey, endpoint } = resolveAuth(flags);
|
|
431
|
+
const headers = buildHeaders(flags, { apiKey });
|
|
432
|
+
const result = await request('GET', `${endpoint}/api/memory/entries/${encodeURIComponent(id)}`, {
|
|
433
|
+
headers,
|
|
434
|
+
});
|
|
435
|
+
emit(result, flags, (r) => JSON.stringify(r, null, 2));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function runDelete(flags, positional) {
|
|
439
|
+
const id = positional[0];
|
|
440
|
+
if (!id) throw new CliError('delete: <id> argument is required', 2);
|
|
441
|
+
const { apiKey, endpoint } = resolveAuth(flags);
|
|
442
|
+
const headers = buildHeaders(flags, { apiKey });
|
|
443
|
+
const result = await request(
|
|
444
|
+
'DELETE',
|
|
445
|
+
`${endpoint}/api/memory/entries/${encodeURIComponent(id)}`,
|
|
446
|
+
{ headers },
|
|
447
|
+
);
|
|
448
|
+
emit(result, flags, () => `deleted ${id}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function runLog(flags) {
|
|
452
|
+
const { apiKey, endpoint } = resolveAuth(flags);
|
|
453
|
+
const headers = buildHeaders(flags, { apiKey });
|
|
454
|
+
const params = new URLSearchParams();
|
|
455
|
+
if (flags.since) params.set('since', String(flags.since));
|
|
456
|
+
if (flags.limit !== undefined) params.set('limit', String(flags.limit));
|
|
457
|
+
if (flags.type) params.set('type', String(flags.type));
|
|
458
|
+
if (flags['agent-id']) params.set('agentId', String(flags['agent-id']));
|
|
459
|
+
if (flags.source) params.set('source', String(flags.source));
|
|
460
|
+
const qs = params.toString();
|
|
461
|
+
const url = `${endpoint}/api/memory/log${qs ? `?${qs}` : ''}`;
|
|
462
|
+
const result = await request('GET', url, { headers });
|
|
463
|
+
emit(result, flags, (r) => {
|
|
464
|
+
const entries = (r && r.data && r.data.entries) || (r && r.entries) || [];
|
|
465
|
+
if (!Array.isArray(entries) || entries.length === 0) return 'no entries';
|
|
466
|
+
return entries
|
|
467
|
+
.map((e) => `${e.createdAt ?? ''} ${e.id ?? ''} ${(e.content ?? '').slice(0, 80)}`)
|
|
468
|
+
.join('\n');
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function runSummarizeEntity(flags, positional) {
|
|
473
|
+
const name = positional[0];
|
|
474
|
+
if (!name) throw new CliError('summarize-entity: <name> argument is required', 2);
|
|
475
|
+
const { apiKey, endpoint } = resolveAuth(flags);
|
|
476
|
+
const headers = buildHeaders(flags, { apiKey, json: true });
|
|
477
|
+
const result = await request('POST', `${endpoint}/api/memory/synthesis/entity`, {
|
|
478
|
+
headers,
|
|
479
|
+
body: JSON.stringify({ name }),
|
|
480
|
+
});
|
|
481
|
+
emit(result, flags, (r) => {
|
|
482
|
+
const summary = (r && r.data && r.data.summary) || (r && r.summary);
|
|
483
|
+
return summary ? `${name}: ${summary}` : JSON.stringify(r);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function runIngestFile(flags, positional) {
|
|
488
|
+
const filePath = positional[0];
|
|
489
|
+
if (!filePath) throw new CliError('ingest-file: <path> argument is required', 2);
|
|
490
|
+
const absPath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
|
|
491
|
+
let fileStat;
|
|
492
|
+
try {
|
|
493
|
+
fileStat = statSync(absPath);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
throw new CliError(`ingest-file: cannot stat '${filePath}': ${err.message}`, 2);
|
|
496
|
+
}
|
|
497
|
+
if (!fileStat.isFile()) throw new CliError(`ingest-file: '${filePath}' is not a regular file`, 2);
|
|
498
|
+
|
|
499
|
+
const { apiKey, endpoint } = resolveAuth(flags);
|
|
500
|
+
const headers = buildHeaders(flags, { apiKey });
|
|
501
|
+
|
|
502
|
+
const buf = readFileSync(absPath);
|
|
503
|
+
const blob = new Blob([buf]);
|
|
504
|
+
const form = new FormData();
|
|
505
|
+
form.append('file', blob, basename(absPath));
|
|
506
|
+
if (flags.description) form.append('description', String(flags.description));
|
|
507
|
+
if (flags['namespace-id']) form.append('namespaceId', String(flags['namespace-id']));
|
|
508
|
+
|
|
509
|
+
let response;
|
|
510
|
+
try {
|
|
511
|
+
response = await fetch(`${endpoint}/api/memory/ingest/file`, {
|
|
512
|
+
method: 'POST',
|
|
513
|
+
headers,
|
|
514
|
+
body: form,
|
|
515
|
+
});
|
|
516
|
+
} catch (err) {
|
|
517
|
+
throw new CliError(`network error: ${err.message}`, 1);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Parse NDJSON stream line-by-line.
|
|
521
|
+
if (!response.body) {
|
|
522
|
+
const text = await response.text();
|
|
523
|
+
throw new CliError(
|
|
524
|
+
`ingest-file: server returned empty body (HTTP ${response.status}): ${text.slice(0, 200)}`,
|
|
525
|
+
1,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const reader = response.body.getReader();
|
|
530
|
+
const decoder = new TextDecoder('utf-8');
|
|
531
|
+
let buffer = '';
|
|
532
|
+
let terminalEvent = null;
|
|
533
|
+
let terminalError = null;
|
|
60
534
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
535
|
+
while (true) {
|
|
536
|
+
const { value, done } = await reader.read();
|
|
537
|
+
if (value) buffer += decoder.decode(value, { stream: true });
|
|
538
|
+
if (done) {
|
|
539
|
+
buffer += decoder.decode();
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
let nl = buffer.indexOf('\n');
|
|
543
|
+
while (nl >= 0) {
|
|
544
|
+
const line = buffer.slice(0, nl).trim();
|
|
545
|
+
buffer = buffer.slice(nl + 1);
|
|
546
|
+
if (line) {
|
|
547
|
+
const event = parseNdjsonLine(line, response.status);
|
|
548
|
+
const handled = handleEvent(event, flags);
|
|
549
|
+
if (handled.terminal) {
|
|
550
|
+
if (event.type === 'error') terminalError = event;
|
|
551
|
+
else terminalEvent = event;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
nl = buffer.indexOf('\n');
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const tail = buffer.trim();
|
|
558
|
+
if (tail) {
|
|
559
|
+
const event = parseNdjsonLine(tail, response.status);
|
|
560
|
+
const handled = handleEvent(event, flags);
|
|
561
|
+
if (handled.terminal) {
|
|
562
|
+
if (event.type === 'error') terminalError = event;
|
|
563
|
+
else terminalEvent = event;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (terminalError) {
|
|
568
|
+
const msg = terminalError.message || terminalError.error || `HTTP ${response.status}`;
|
|
569
|
+
throw new CliError(`ingest-file error: ${msg}`, 1);
|
|
570
|
+
}
|
|
571
|
+
if (!terminalEvent) {
|
|
572
|
+
throw new CliError(
|
|
573
|
+
`ingest-file: stream ended without a terminal result (HTTP ${response.status})`,
|
|
574
|
+
1,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
if (!response.ok) {
|
|
578
|
+
throw new CliError(`ingest-file: HTTP ${response.status} despite terminal event`, 1);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
emit(terminalEvent, flags, (r) => {
|
|
582
|
+
const chunks = r.chunks ?? '?';
|
|
583
|
+
const fname = r.filename ?? '<file>';
|
|
584
|
+
return `ingested ${fname}: ${chunks} chunks`;
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function parseNdjsonLine(line, status) {
|
|
589
|
+
try {
|
|
590
|
+
return JSON.parse(line);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
throw new CliError(`ingest-file: malformed NDJSON line (HTTP ${status}): ${err.message}`, 1);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function handleEvent(event, flags) {
|
|
597
|
+
const type = event && event.type;
|
|
598
|
+
if (type === 'progress' || type === 'heartbeat') {
|
|
599
|
+
if (!flags.json) {
|
|
600
|
+
const stage = event.stage ?? '?';
|
|
601
|
+
const msg = event.message ?? '';
|
|
602
|
+
process.stderr.write(`[${type}:${stage}] ${msg}\n`);
|
|
603
|
+
}
|
|
604
|
+
return { terminal: false };
|
|
605
|
+
}
|
|
606
|
+
if (type === 'result' || type === 'error') {
|
|
607
|
+
return { terminal: true };
|
|
608
|
+
}
|
|
609
|
+
// Unknown event type — surface but don't terminate.
|
|
610
|
+
if (!flags.json) {
|
|
611
|
+
process.stderr.write(`[event] ${JSON.stringify(event)}\n`);
|
|
612
|
+
}
|
|
613
|
+
return { terminal: false };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ---------- Dispatcher ----------
|
|
617
|
+
|
|
618
|
+
async function main() {
|
|
619
|
+
const argv = process.argv.slice(2);
|
|
620
|
+
const { flags, positional } = parseArgs(argv);
|
|
621
|
+
const command = positional.shift();
|
|
622
|
+
|
|
623
|
+
if (!command || command === '--help' || command === 'help' || (flags.help === true && !command)) {
|
|
624
|
+
printHelp();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (flags.help === true) {
|
|
629
|
+
printHelp(command);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
switch (command) {
|
|
634
|
+
case 'init':
|
|
635
|
+
runInit();
|
|
636
|
+
return;
|
|
637
|
+
case 'store':
|
|
638
|
+
await runStore(flags);
|
|
639
|
+
return;
|
|
640
|
+
case 'search':
|
|
641
|
+
await runSearch(flags);
|
|
642
|
+
return;
|
|
643
|
+
case 'get':
|
|
644
|
+
await runGet(flags, positional);
|
|
645
|
+
return;
|
|
646
|
+
case 'delete':
|
|
647
|
+
await runDelete(flags, positional);
|
|
648
|
+
return;
|
|
649
|
+
case 'log':
|
|
650
|
+
await runLog(flags);
|
|
651
|
+
return;
|
|
652
|
+
case 'summarize-entity':
|
|
653
|
+
await runSummarizeEntity(flags, positional);
|
|
654
|
+
return;
|
|
655
|
+
case 'ingest-file':
|
|
656
|
+
await runIngestFile(flags, positional);
|
|
657
|
+
return;
|
|
658
|
+
default:
|
|
659
|
+
process.stderr.write(`error: unknown command '${command}'\n`);
|
|
660
|
+
printHelp();
|
|
661
|
+
process.exit(2);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
main().catch((err) => {
|
|
666
|
+
if (err instanceof CliError) {
|
|
667
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
668
|
+
process.exit(err.exitCode);
|
|
669
|
+
}
|
|
670
|
+
process.stderr.write(`error: ${err && err.stack ? err.stack : String(err)}\n`);
|
|
671
|
+
process.exit(1);
|
|
672
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -356,11 +356,14 @@ declare const VectorProvider: {
|
|
|
356
356
|
type VectorProvider = (typeof VectorProvider)[keyof typeof VectorProvider];
|
|
357
357
|
declare const EmbeddingProviderName: {
|
|
358
358
|
readonly STUB: "stub";
|
|
359
|
-
/** @deprecated Vestigial — pyx-memory uses internal
|
|
359
|
+
/** @deprecated Vestigial — pyx-memory uses internal EmbeddingGemma embeddings. */
|
|
360
360
|
readonly ANTHROPIC: "anthropic";
|
|
361
|
-
/** @deprecated Vestigial — pyx-memory uses internal
|
|
361
|
+
/** @deprecated Vestigial — pyx-memory uses internal EmbeddingGemma embeddings. */
|
|
362
362
|
readonly OPENAI: "openai";
|
|
363
|
+
/** In-process ONNX model (default: EmbeddingGemma-300M). */
|
|
363
364
|
readonly LOCAL: "local";
|
|
365
|
+
/** Remote OpenAI-compatible embedding service (pyx-cloud shared, custom, etc.). */
|
|
366
|
+
readonly HTTP: "http";
|
|
364
367
|
};
|
|
365
368
|
type EmbeddingProviderName = (typeof EmbeddingProviderName)[keyof typeof EmbeddingProviderName];
|
|
366
369
|
interface MemoryEntry {
|
package/dist/index.mjs
CHANGED
|
@@ -40,11 +40,14 @@ var VectorProvider = {
|
|
|
40
40
|
};
|
|
41
41
|
var EmbeddingProviderName = {
|
|
42
42
|
STUB: "stub",
|
|
43
|
-
/** @deprecated Vestigial — pyx-memory uses internal
|
|
43
|
+
/** @deprecated Vestigial — pyx-memory uses internal EmbeddingGemma embeddings. */
|
|
44
44
|
ANTHROPIC: "anthropic",
|
|
45
|
-
/** @deprecated Vestigial — pyx-memory uses internal
|
|
45
|
+
/** @deprecated Vestigial — pyx-memory uses internal EmbeddingGemma embeddings. */
|
|
46
46
|
OPENAI: "openai",
|
|
47
|
-
|
|
47
|
+
/** In-process ONNX model (default: EmbeddingGemma-300M). */
|
|
48
|
+
LOCAL: "local",
|
|
49
|
+
/** Remote OpenAI-compatible embedding service (pyx-cloud shared, custom, etc.). */
|
|
50
|
+
HTTP: "http"
|
|
48
51
|
};
|
|
49
52
|
var StoreTarget = {
|
|
50
53
|
SQLITE: "sqlite",
|
package/package.json
CHANGED
|
@@ -37,6 +37,9 @@ Your endpoint and API key are in the project's CLAUDE.md or agent config. Look f
|
|
|
37
37
|
|
|
38
38
|
If no credentials are configured, skip to [SDK Integration](#sdk-integration) below.
|
|
39
39
|
|
|
40
|
+
Operations below use the `pyxmate-memory` CLI (ships with `@pyxmate/memory`).
|
|
41
|
+
Set `PYXMATE_API_KEY` and `PYXMATE_ENDPOINT` once; for multi-tenant servers add `--tenant-id <id>`.
|
|
42
|
+
|
|
40
43
|
## Store: after important events
|
|
41
44
|
|
|
42
45
|
Immediately after any of these events, store a memory before continuing:
|
|
@@ -50,23 +53,11 @@ Immediately after any of these events, store a memory before continuing:
|
|
|
50
53
|
- **The user explicitly says "remember this"** → store immediately
|
|
51
54
|
|
|
52
55
|
```bash
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-H "Content-Type: application/json" \
|
|
56
|
-
-d '{"content":"WHAT_HAPPENED","type":"long-term","metadata":{"source":"agent","topic":"TOPIC","project":"PROJECT_NAME"}}'
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**Multi-tenant mode**: When the server uses `TENANT_MODE=multi`, include `X-Tenant-Id` on all requests:
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
curl -s -X POST {{ENDPOINT}}/api/memory/ingest \
|
|
63
|
-
-H "Authorization: Bearer {{API_KEY}}" \
|
|
64
|
-
-H "Content-Type: application/json" \
|
|
65
|
-
-H "X-Tenant-Id: {{TENANT_ID}}" \
|
|
66
|
-
-d '{"content":"WHAT_HAPPENED","type":"long-term","metadata":{"source":"agent","topic":"TOPIC","project":"PROJECT_NAME"}}'
|
|
56
|
+
pyxmate-memory store \
|
|
57
|
+
--content "WHAT_HAPPENED" --topic TOPIC --project PROJECT_NAME --source agent
|
|
67
58
|
```
|
|
68
59
|
|
|
69
|
-
When the memory mentions
|
|
60
|
+
When the memory mentions named people, tools, or organizations, pass `--entities '<json>'` and `--relationships '<json>'` (or `@path.json` / `-` for stdin) to populate the knowledge graph. See [reference/http-api.md](reference/http-api.md) for entity and relationship types.
|
|
70
61
|
|
|
71
62
|
## Search: before making assumptions
|
|
72
63
|
|
|
@@ -78,12 +69,9 @@ Search for relevant context in these situations:
|
|
|
78
69
|
- **Investigating a bug** → search if it was seen before
|
|
79
70
|
|
|
80
71
|
```bash
|
|
81
|
-
|
|
82
|
-
-H "Authorization: Bearer {{API_KEY}}"
|
|
72
|
+
pyxmate-memory search --query "QUERY" --limit 5
|
|
83
73
|
```
|
|
84
74
|
|
|
85
|
-
**Multi-tenant mode**: Add `-H "X-Tenant-Id: {{TENANT_ID}}"` to scope search results to the tenant.
|
|
86
|
-
|
|
87
75
|
## Ingest files & images: when a file is worth remembering
|
|
88
76
|
|
|
89
77
|
Upload files directly when the content is worth persisting — diagrams, screenshots, documents, data files.
|
|
@@ -93,10 +81,7 @@ Upload files directly when the content is worth persisting — diagrams, screens
|
|
|
93
81
|
- **A document contains reference material** (spec, config, report) → ingest it
|
|
94
82
|
|
|
95
83
|
```bash
|
|
96
|
-
|
|
97
|
-
-H "Authorization: Bearer {{API_KEY}}" \
|
|
98
|
-
-F "file=@path/to/file" \
|
|
99
|
-
-F "description=What this file contains or shows"
|
|
84
|
+
pyxmate-memory ingest-file path/to/file --description "What this file contains or shows"
|
|
100
85
|
```
|
|
101
86
|
|
|
102
87
|
Supported formats: txt, md, csv, tsv, log, pdf, docx, xlsx, pptx, json, jsonl, html, htm, png, jpg, jpeg, webp, gif, bmp, tiff, svg (100MB limit).
|