@avee1234/memport 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abhi Das
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # memport
2
+
3
+ [![CI](https://github.com/abhid1234/memport/actions/workflows/ci.yml/badge.svg)](https://github.com/abhid1234/memport/actions/workflows/ci.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) ![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)
4
+
5
+ **[▶ Play with it live →](https://memport.vercel.app)** · export → import → conformance round-trip on the real library, in your browser.
6
+
7
+ **The open, portable format for AI agent memory.** Export memory from any store, import it into any harness. Zero dependencies.
8
+
9
+ > Working name — the concept was queued as "openmemory," but that collides with Mem0's shipping product, so this is deliberately differentiated. See [`vision.md`](vision.md).
10
+
11
+ Agent memory today is locked in: Mem0 keeps it in its cloud, Zep in a proprietary graph, and every harness in its own private store. There's no vendor-neutral way to move, back up, or audit what an agent remembers. memport is the thin open layer *between* stores — a format, not another backend.
12
+
13
+ ```bash
14
+ memport export --from ./mem-store # any store → the neutral .memport format
15
+ memport import --to ./other-store # .memport → any target store
16
+ memport conformance a.memport b.memport # how much memory survived the round-trip?
17
+ memport diff a.memport b.memport # what was added / dropped / changed
18
+ ```
19
+
20
+ Same playbook as [opentrajectory](https://github.com/abhid1234/opentrajectory) (format for agent traces) and [constraintguard](https://github.com/abhid1234/constraintguard) (format for constraints): own the open interoperability standard, not the storage.
21
+
22
+ Status: **drafting** — see [`roadmap.md`](roadmap.md). Grounded in the memory-portability gap (arXiv:2605.11032).
23
+
24
+ MIT · zero dependencies · harness-neutral
package/bin/memport.js ADDED
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+ // memport CLI — a thin subcommand dispatcher. `validate`, `export`, `import`,
3
+ // and `conformance` today; new subcommands slot in without a rewrite.
4
+ //
5
+ // memport validate <file> [--json]
6
+ // memport export --from <dir-or-file> [--to <file>] [--adapter <name>]
7
+ // memport import --from <set-file> --to <target> [--adapter <name>] [--mode merge|replace|fail] [--json]
8
+ // memport conformance <before.memport> <after.memport> [--json] [--min <0..1>]
9
+ //
10
+ // Exit codes:
11
+ // 0 — success (input valid / export written / import written / scored)
12
+ // 1 — input parsed but failed validation (validate, export's produced set,
13
+ // import's input set, conformance's input sets), or `--mode fail` blocked
14
+ // by a non-empty target, or a `--min` gate the `score` fell short of
15
+ // 2 — usage/IO error: no path, unknown subcommand, file not found, invalid JSON
16
+
17
+ import { readFileSync, writeFileSync } from 'node:fs';
18
+ import {
19
+ validateMemoryRecord,
20
+ validateMemorySet,
21
+ exportMemory,
22
+ ExportError,
23
+ importMemory,
24
+ ImportError,
25
+ scoreConformance,
26
+ ConformanceError,
27
+ } from '../src/index.js';
28
+
29
+ const USAGE = [
30
+ 'usage: memport validate <file> [--json]',
31
+ ' memport export --from <dir-or-file> [--to <file>] [--adapter <name>]',
32
+ ' memport import --from <set-file> --to <target> [--adapter <name>] [--mode merge|replace|fail] [--json]',
33
+ ' memport conformance <before.memport> <after.memport> [--json] [--min <0..1>]',
34
+ ].join('\n');
35
+
36
+ function fail(message, code) {
37
+ process.stderr.write(`${message}\n`);
38
+ process.exit(code);
39
+ }
40
+
41
+ function runValidate(args) {
42
+ const json = args.includes('--json');
43
+ const paths = args.filter((a) => a !== '--json');
44
+
45
+ if (paths.length === 0) {
46
+ fail(`error: no file given\n${USAGE}`, 2);
47
+ }
48
+ const file = paths[0];
49
+
50
+ let raw;
51
+ try {
52
+ raw = readFileSync(file, 'utf8');
53
+ } catch {
54
+ fail(`error: cannot read file: ${file}`, 2);
55
+ }
56
+
57
+ let data;
58
+ try {
59
+ data = JSON.parse(raw);
60
+ } catch (e) {
61
+ fail(`error: invalid JSON in ${file}: ${e.message}`, 2);
62
+ }
63
+
64
+ const isSet = Array.isArray(data);
65
+ const result = isSet ? validateMemorySet(data) : validateMemoryRecord(data);
66
+
67
+ if (json) {
68
+ process.stdout.write(`${JSON.stringify(result)}\n`);
69
+ process.exit(result.valid ? 0 : 1);
70
+ }
71
+
72
+ if (result.valid) {
73
+ const count = isSet ? data.length : 1;
74
+ process.stdout.write(`✓ valid (${count} record${count === 1 ? '' : 's'})\n`);
75
+ process.exit(0);
76
+ }
77
+
78
+ process.stdout.write(`✗ invalid (${result.errors.length} error${result.errors.length === 1 ? '' : 's'})\n`);
79
+ result.errors.forEach((e, i) => {
80
+ const loc = e.index !== undefined ? `[${e.index}]${e.path ? '.' + e.path : ''}` : e.path || '(record)';
81
+ process.stdout.write(` ${i + 1}. ${loc} — ${e.message}\n`);
82
+ });
83
+ process.exit(1);
84
+ }
85
+
86
+ // Parse `--flag value` options; returns { from, to, adapter } (undefined if
87
+ // absent). Unknown flags and a missing value are usage errors.
88
+ function parseExportArgs(args) {
89
+ const opts = {};
90
+ const known = { '--from': 'from', '--to': 'to', '--adapter': 'adapter' };
91
+ for (let i = 0; i < args.length; i++) {
92
+ const key = known[args[i]];
93
+ if (!key) fail(`error: unknown option: ${args[i]}\n${USAGE}`, 2);
94
+ const value = args[i + 1];
95
+ if (value === undefined || value.startsWith('--')) {
96
+ fail(`error: ${args[i]} requires a value\n${USAGE}`, 2);
97
+ }
98
+ opts[key] = value;
99
+ i++;
100
+ }
101
+ return opts;
102
+ }
103
+
104
+ function runExport(args) {
105
+ const { from, to, adapter } = parseExportArgs(args);
106
+
107
+ if (!from) {
108
+ fail(`error: --from is required\n${USAGE}`, 2);
109
+ }
110
+
111
+ let set;
112
+ try {
113
+ set = exportMemory(from, adapter ? { adapter } : {});
114
+ } catch (e) {
115
+ if (e instanceof ExportError && e.code === 'invalid_set') {
116
+ // The source produced an invalid set — print the #1 errors, write nothing.
117
+ process.stdout.write(`✗ export produced an invalid memory set (${e.errors.length} error${e.errors.length === 1 ? '' : 's'})\n`);
118
+ e.errors.forEach((err, i) => {
119
+ const loc = err.index !== undefined ? `[${err.index}]${err.path ? '.' + err.path : ''}` : err.path || '(record)';
120
+ process.stdout.write(` ${i + 1}. ${loc} — ${err.message}\n`);
121
+ });
122
+ process.exit(1);
123
+ }
124
+ // unknown_adapter / source_not_found / invalid_json — usage/IO errors.
125
+ fail(`error: ${e.message}`, 2);
126
+ }
127
+
128
+ const output = `${JSON.stringify(set, null, 2)}\n`;
129
+
130
+ if (to) {
131
+ try {
132
+ writeFileSync(to, output);
133
+ } catch (e) {
134
+ fail(`error: cannot write file: ${to}: ${e.message}`, 2);
135
+ }
136
+ process.stderr.write(`✓ exported ${set.length} record${set.length === 1 ? '' : 's'} → ${to}\n`);
137
+ } else {
138
+ process.stdout.write(output);
139
+ }
140
+ process.exit(0);
141
+ }
142
+
143
+ // Print the #1 structured errors under a header line (shared by validate,
144
+ // export's invalid-output path, and import's invalid-input path).
145
+ function printErrors(header, errors) {
146
+ process.stdout.write(`${header} (${errors.length} error${errors.length === 1 ? '' : 's'})\n`);
147
+ errors.forEach((e, i) => {
148
+ const loc = e.index !== undefined ? `[${e.index}]${e.path ? '.' + e.path : ''}` : e.path || '(record)';
149
+ process.stdout.write(` ${i + 1}. ${loc} — ${e.message}\n`);
150
+ });
151
+ }
152
+
153
+ // Parse `--flag value` options for import; adds `--mode` and the boolean
154
+ // `--json` on top of export's `--from/--to/--adapter`.
155
+ function parseImportArgs(args) {
156
+ const opts = {};
157
+ const known = { '--from': 'from', '--to': 'to', '--adapter': 'adapter', '--mode': 'mode' };
158
+ for (let i = 0; i < args.length; i++) {
159
+ if (args[i] === '--json') {
160
+ opts.json = true;
161
+ continue;
162
+ }
163
+ const key = known[args[i]];
164
+ if (!key) fail(`error: unknown option: ${args[i]}\n${USAGE}`, 2);
165
+ const value = args[i + 1];
166
+ if (value === undefined || value.startsWith('--')) {
167
+ fail(`error: ${args[i]} requires a value\n${USAGE}`, 2);
168
+ }
169
+ opts[key] = value;
170
+ i++;
171
+ }
172
+ return opts;
173
+ }
174
+
175
+ function runImport(args) {
176
+ const { from, to, adapter, mode, json } = parseImportArgs(args);
177
+
178
+ if (!from) fail(`error: --from is required\n${USAGE}`, 2);
179
+ if (!to) fail(`error: --to is required\n${USAGE}`, 2);
180
+
181
+ // Load the set from --from. It must parse to a JSON array; a non-array (or
182
+ // unreadable / unparseable) source is a usage error (exit 2). The array's
183
+ // contents are validated inside importMemory.
184
+ let raw;
185
+ try {
186
+ raw = readFileSync(from, 'utf8');
187
+ } catch {
188
+ fail(`error: cannot read file: ${from}`, 2);
189
+ }
190
+ let set;
191
+ try {
192
+ set = JSON.parse(raw);
193
+ } catch (e) {
194
+ fail(`error: invalid JSON in ${from}: ${e.message}`, 2);
195
+ }
196
+ if (!Array.isArray(set)) {
197
+ fail(`error: ${from} must contain a memory set (a JSON array of records)`, 2);
198
+ }
199
+
200
+ const opts = {};
201
+ if (adapter) opts.adapter = adapter;
202
+ if (mode) opts.mode = mode;
203
+
204
+ let report;
205
+ try {
206
+ report = importMemory(set, to, opts);
207
+ } catch (e) {
208
+ if (e instanceof ImportError && e.code === 'invalid_set') {
209
+ // The input set failed validation — print the #1 errors, write nothing.
210
+ printErrors('✗ import received an invalid memory set', e.errors);
211
+ process.exit(1);
212
+ }
213
+ if (e instanceof ImportError && e.code === 'target_not_empty') {
214
+ // --mode fail blocked by a non-empty target — nothing written.
215
+ fail(`error: ${e.message}`, 1);
216
+ }
217
+ // unknown_adapter / unknown_mode / target_is_directory / write_failed /
218
+ // invalid_json — usage/IO errors.
219
+ if (e instanceof ImportError) fail(`error: ${e.message}`, 2);
220
+ throw e;
221
+ }
222
+
223
+ if (json) {
224
+ process.stdout.write(`${JSON.stringify(report)}\n`);
225
+ process.exit(0);
226
+ }
227
+
228
+ process.stderr.write(
229
+ `✓ imported ${report.written} record${report.written === 1 ? '' : 's'} ` +
230
+ `(${report.skipped} skipped, ${report.unsupported.length} unsupported) → ${to}\n`,
231
+ );
232
+ process.exit(0);
233
+ }
234
+
235
+ // Parse `conformance`'s two positional file paths plus the boolean `--json`
236
+ // and the valued `--min`. Positionals (file paths) and `--flag`s can interleave;
237
+ // an unknown option, a missing `--min` value, or the wrong positional count are
238
+ // usage errors caught here or by the caller.
239
+ function parseConformanceArgs(args) {
240
+ const positionals = [];
241
+ const opts = { json: false };
242
+ for (let i = 0; i < args.length; i++) {
243
+ const arg = args[i];
244
+ if (arg === '--json') {
245
+ opts.json = true;
246
+ } else if (arg === '--min') {
247
+ const value = args[i + 1];
248
+ if (value === undefined || value.startsWith('--')) {
249
+ fail(`error: --min requires a value\n${USAGE}`, 2);
250
+ }
251
+ opts.min = value;
252
+ i++;
253
+ } else if (arg.startsWith('--')) {
254
+ fail(`error: unknown option: ${arg}\n${USAGE}`, 2);
255
+ } else {
256
+ positionals.push(arg);
257
+ }
258
+ }
259
+ return { positionals, opts };
260
+ }
261
+
262
+ // Load a memport set file for conformance: readable, parseable JSON, and a JSON
263
+ // array. Its records are validated inside `scoreConformance`. Anything else is a
264
+ // usage/IO error (exit 2).
265
+ function loadSetFile(file) {
266
+ let raw;
267
+ try {
268
+ raw = readFileSync(file, 'utf8');
269
+ } catch {
270
+ fail(`error: cannot read file: ${file}`, 2);
271
+ }
272
+ let data;
273
+ try {
274
+ data = JSON.parse(raw);
275
+ } catch (e) {
276
+ fail(`error: invalid JSON in ${file}: ${e.message}`, 2);
277
+ }
278
+ if (!Array.isArray(data)) {
279
+ fail(`error: ${file} must contain a memory set (a JSON array of records)`, 2);
280
+ }
281
+ return data;
282
+ }
283
+
284
+ function runConformance(args) {
285
+ const { positionals, opts } = parseConformanceArgs(args);
286
+
287
+ if (positionals.length !== 2) {
288
+ fail(`error: conformance takes exactly two files: <before> <after>\n${USAGE}`, 2);
289
+ }
290
+ const [beforeFile, afterFile] = positionals;
291
+
292
+ // Optional `--min` CI gate: a float in [0, 1]. A non-numeric or out-of-range
293
+ // value is a usage error (exit 2), distinct from the gate itself failing.
294
+ let min;
295
+ if (opts.min !== undefined) {
296
+ min = Number(opts.min);
297
+ if (!Number.isFinite(min) || min < 0 || min > 1) {
298
+ fail(`error: --min must be a number in [0, 1], got: ${opts.min}\n${USAGE}`, 2);
299
+ }
300
+ }
301
+
302
+ const before = loadSetFile(beforeFile);
303
+ const after = loadSetFile(afterFile);
304
+
305
+ let report;
306
+ try {
307
+ report = scoreConformance(before, after);
308
+ } catch (e) {
309
+ if (e instanceof ConformanceError && e.code === 'invalid_set') {
310
+ // An input set failed validation — print the #1 errors naming the side/file.
311
+ const file = e.side === 'before' ? beforeFile : afterFile;
312
+ printErrors(`✗ ${e.side} set is invalid (${file})`, e.errors);
313
+ process.exit(1);
314
+ }
315
+ if (e instanceof ConformanceError) fail(`error: ${e.message}`, 2);
316
+ throw e;
317
+ }
318
+
319
+ const gatePassed = min === undefined || report.score >= min;
320
+
321
+ if (opts.json) {
322
+ process.stdout.write(`${JSON.stringify(report)}\n`);
323
+ process.exit(gatePassed ? 0 : 1);
324
+ }
325
+
326
+ const pct = (report.score * 100).toFixed(1);
327
+ const mark = report.score === 1 || gatePassed ? '✓' : '✗';
328
+ process.stdout.write(
329
+ `${mark} conformance ${pct}% (${report.preserved}/${report.total} preserved, ` +
330
+ `${report.degraded.length} degraded, ${report.lost.length} lost)\n`,
331
+ );
332
+
333
+ if (report.lost.length > 0) {
334
+ process.stdout.write(`lost (${report.lost.length}):\n`);
335
+ for (const r of report.lost) {
336
+ process.stdout.write(` - ${r.id} (${r.kind}) — "${r.content}"\n`);
337
+ }
338
+ }
339
+ if (report.degraded.length > 0) {
340
+ process.stdout.write(`degraded (${report.degraded.length}):\n`);
341
+ for (const r of report.degraded) {
342
+ process.stdout.write(` - ${r.id} — ${r.fields.join(', ')}\n`);
343
+ }
344
+ }
345
+
346
+ if (!gatePassed) {
347
+ process.stderr.write(
348
+ `error: conformance ${pct}% is below the --min ${(min * 100).toFixed(1)}% threshold\n`,
349
+ );
350
+ process.exit(1);
351
+ }
352
+ process.exit(0);
353
+ }
354
+
355
+ function main() {
356
+ const [subcommand, ...rest] = process.argv.slice(2);
357
+
358
+ switch (subcommand) {
359
+ case 'validate':
360
+ return runValidate(rest);
361
+ case 'export':
362
+ return runExport(rest);
363
+ case 'import':
364
+ return runImport(rest);
365
+ case 'conformance':
366
+ return runConformance(rest);
367
+ default:
368
+ fail(
369
+ subcommand
370
+ ? `error: unknown subcommand: ${subcommand}\n${USAGE}`
371
+ : USAGE,
372
+ 2,
373
+ );
374
+ }
375
+ }
376
+
377
+ main();
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@avee1234/memport",
3
+ "version": "0.1.0",
4
+ "description": "The open, portable format for AI agent memory — export from any store, import into any harness. Zero dependencies.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "memport": "bin/memport.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "scripts": {
20
+ "test": "node --test",
21
+ "copy:lib": "node site/sync-lib.mjs"
22
+ },
23
+ "keywords": [
24
+ "ai",
25
+ "agent",
26
+ "memory",
27
+ "portability",
28
+ "interoperability",
29
+ "schema",
30
+ "mem0",
31
+ "open-format",
32
+ "conformance",
33
+ "zero-dependency"
34
+ ],
35
+ "license": "MIT",
36
+ "homepage": "https://memport.vercel.app",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/abhid1234/memport.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/abhid1234/memport/issues"
43
+ }
44
+ }