@vib795/agent-memory 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +30 -0
  2. package/package.json +1 -1
  3. package/src/cli.js +150 -0
package/README.md CHANGED
@@ -94,6 +94,36 @@ recursive CTE *is* the graph engine, and `UNION` plus a depth bound is what keep
94
94
  - **Constraints are never dropped.** At any cap, in either tier. A constraint is what
95
95
  stops an agent burning a retry loop on an approach that was never going to ship.
96
96
 
97
+ ## Carrying knowledge between machines
98
+
99
+ Client work usually lives in its own environment, and when the engagement ends the
100
+ environment goes with it. What should survive is the part that was never the
101
+ client's — the constraints your org imposes, the conventions you follow, what you
102
+ have learned about working under them. What must not survive is their architecture.
103
+
104
+ ```bash
105
+ agent-memory export --out carry.json # global scope only, by default
106
+ agent-memory import carry.json --dry-run # see what would land
107
+ agent-memory import carry.json
108
+ ```
109
+
110
+ **The default is the safe one.** `export` emits `scope: global` notes and nothing
111
+ else, so a note tied to a client repository cannot leave by forgetting a flag.
112
+ Taking one is possible — `--scope all` — but it takes saying so.
113
+
114
+ Two things are deliberately dropped on the way through:
115
+
116
+ - **`captured_sha` is not carried.** It names a commit that exists in one repository
117
+ on one machine. Carried across, it would either read as current forever or claim
118
+ the history was rewritten; a staleness signal you cannot check is worse than none.
119
+ - **`source` becomes `import`.** How a note was originally captured describes an
120
+ environment that no longer exists. Here the honest answer to where it came from is
121
+ that someone brought it in, and an audit should be able to tell which notes those
122
+ are.
123
+
124
+ Importing the same file twice updates rather than duplicates, so re-running after a
125
+ change is safe.
126
+
97
127
  ## Engagements
98
128
 
99
129
  If you work for more than one client on one machine, knowledge from one of them is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vib795/agent-memory",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Durable cross-repo knowledge graph for GitHub Copilot and Claude Code. Markdown source of truth, disposable SQLite index, zero runtime dependencies.",
5
5
  "keywords": [
6
6
  "github-copilot",
package/src/cli.js CHANGED
@@ -86,6 +86,9 @@ const USAGE = `agent-memory — durable cross-repo knowledge for coding agents
86
86
  [--source <name>] [--repo <name>]
87
87
  compact dedup, decay, reindex, regenerate
88
88
  doctor preflight and health report
89
+ export [--scope global|repo|all] knowledge worth carrying to another machine
90
+ [--out <file>] defaults to global scope only
91
+ import <file> [--dry-run] bring an export in, provenance intact
89
92
  engagement [show|list|use <name>] which client store this window writes to
90
93
  [purge <name> --yes] delete one engagement's store entirely
91
94
 
@@ -561,6 +564,151 @@ function cmdDoctor() {
561
564
 
562
565
  // --- dispatch ---------------------------------------------------------------
563
566
 
567
+ const EXPORT_SCOPES = ['global', 'repo', 'all'];
568
+
569
+ /**
570
+ * Take the knowledge that is yours to take.
571
+ *
572
+ * Client work tends to live in its own environment, and when the engagement ends the
573
+ * environment goes with it. What should survive is what was never the client's: your
574
+ * conventions, the constraints an org imposes, the way you have learned to work.
575
+ * What must not survive is their architecture.
576
+ *
577
+ * So this defaults to global scope and nothing else. Exporting a client's notes is
578
+ * possible, because sometimes it is legitimately yours to move, but it takes saying
579
+ * so out loud rather than forgetting a flag.
580
+ *
581
+ * `captured_sha` is deliberately dropped. It names a commit that does not exist
582
+ * anywhere else, and a staleness signal that cannot be checked is worse than none:
583
+ * it would either read as current forever or claim the history was rewritten.
584
+ */
585
+ function cmdExport(opts) {
586
+ const scope = opts.scope === true ? 'global' : (opts.scope ?? 'global');
587
+ if (!EXPORT_SCOPES.includes(scope)) {
588
+ return {
589
+ ok: false,
590
+ error: `scope must be one of ${EXPORT_SCOPES.join('|')}`,
591
+ text: `--scope must be one of ${EXPORT_SCOPES.join(', ')}. Default is global.`,
592
+ };
593
+ }
594
+
595
+ const all = listNotes().filter((n) => !n.__error && n.id);
596
+ const active = opts['include-archived'] ? all : all.filter((n) => !n.archived);
597
+ const picked = active.filter((n) => {
598
+ const s = n.scope || (n.repos?.length ? 'repo' : 'global');
599
+ return scope === 'all' || s === scope;
600
+ });
601
+
602
+ const nodes = picked.map((n) => ({
603
+ id: n.id,
604
+ type: n.type,
605
+ title: n.title,
606
+ body: n.body,
607
+ scope: n.scope || (n.repos?.length ? 'repo' : 'global'),
608
+ repos: n.repos ?? [],
609
+ confidence: n.confidence ?? 'observed',
610
+ supersedes: n.supersedes ?? null,
611
+ edges: n.edges ?? [],
612
+ source: n.source ?? 'manual',
613
+ }));
614
+
615
+ const payload = `${JSON.stringify({ nodes }, null, 2)}\n`;
616
+ if (typeof opts.out === 'string') {
617
+ atomicWrite(opts.out, payload);
618
+ return {
619
+ ok: true,
620
+ exported: nodes.length,
621
+ scope,
622
+ out: opts.out,
623
+ text: `Exported ${nodes.length} ${scope}-scope note${nodes.length === 1 ? '' : 's'} to ${opts.out}.`,
624
+ };
625
+ }
626
+ // Straight to stdout so it pipes, with the count on stderr where it will not
627
+ // corrupt the document.
628
+ process.stderr.write(`${nodes.length} ${scope}-scope notes\n`);
629
+ return { ok: true, exported: nodes.length, scope, nodes, text: payload.trimEnd() };
630
+ }
631
+
632
+ /**
633
+ * Bring exported knowledge into this environment.
634
+ *
635
+ * Separate from `write` because `write` stamps the current repository and commit onto
636
+ * whatever it is given, which is right for capture and wrong for this: it would
637
+ * relabel another environment's knowledge as having been observed here.
638
+ */
639
+ function cmdImport(opts) {
640
+ const file = opts._[0] ?? opts.from;
641
+ if (!file || file === true || (file !== '-' && !existsSync(file))) {
642
+ return {
643
+ ok: false,
644
+ error: 'import requires a file, or - to read stdin',
645
+ text: 'Usage: agent-memory import <file> (or - to read stdin)',
646
+ };
647
+ }
648
+
649
+ let incoming;
650
+ try {
651
+ incoming = readNodesFrom(file);
652
+ } catch (err) {
653
+ return { ok: false, error: `unreadable JSON: ${err.message}`, text: `Unreadable JSON: ${err.message}` };
654
+ }
655
+
656
+ const db = openDb();
657
+ const existing = new Set(db.prepare('SELECT id FROM nodes').all().map((r) => r.id));
658
+
659
+ if (opts['dry-run']) {
660
+ db.close();
661
+ const lines = incoming.map(
662
+ (n) => `${existing.has(n.id) ? 'would update' : 'would create'} ${n.id} [${n.type}]`,
663
+ );
664
+ return {
665
+ ok: true,
666
+ dryRun: true,
667
+ count: incoming.length,
668
+ text: [...lines, '', 'Nothing written. Re-run without --dry-run to apply.'].join('\n'),
669
+ };
670
+ }
671
+
672
+ const written = [];
673
+ const failed = [];
674
+ const warnings = [];
675
+ for (const raw of incoming) {
676
+ // Nothing from this environment is stamped on: no repo, no commit. An imported
677
+ // note claims only what it claimed where it was written.
678
+ // Stamped `import`, not the source it carried. How it was captured describes an
679
+ // environment that no longer exists; here, the honest answer to where this came
680
+ // from is that someone brought it in, and an audit should be able to see which.
681
+ const node = { ...raw, source: 'import' };
682
+ delete node.captured_sha;
683
+ try {
684
+ const res = writeNote(node);
685
+ written.push({ id: res.node.id, type: res.node.type, created: res.created });
686
+ if (res.findings.length) {
687
+ warnings.push(
688
+ `${res.node.id}: redacted ${res.findings.map((f) => `${f.count}x ${f.kind}`).join(', ')}`,
689
+ );
690
+ }
691
+ } catch (err) {
692
+ failed.push({ id: raw?.id ?? null, errors: err.errors ?? [err.message] });
693
+ }
694
+ }
695
+
696
+ reindex(db);
697
+ db.close();
698
+
699
+ return {
700
+ ok: failed.length === 0,
701
+ imported: written.length,
702
+ failed,
703
+ warnings,
704
+ text: [
705
+ ...written.map((w) => `${w.created ? 'created' : 'updated'} ${w.id} [${w.type}]`),
706
+ ...warnings.map((w) => `warning: ${w}`),
707
+ ...failed.map((f) => `failed ${f.id}: ${f.errors.join('; ')}`),
708
+ ].join('\n'),
709
+ };
710
+ }
711
+
564
712
  /**
565
713
  * Show, switch and purge engagements.
566
714
  *
@@ -721,6 +869,8 @@ const COMMANDS = {
721
869
  compact: cmdCompact,
722
870
  doctor: cmdDoctor,
723
871
  engagement: cmdEngagement,
872
+ export: cmdExport,
873
+ import: cmdImport,
724
874
  };
725
875
 
726
876
  function main(argv) {