@mnemahq/cli 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.md +83 -0
- package/README.md +63 -0
- package/bin/mnema.mjs +7 -0
- package/package.json +37 -0
- package/src/artifacts.mjs +148 -0
- package/src/cli.mjs +354 -0
- package/src/hook-install.mjs +111 -0
- package/src/keychain.mjs +207 -0
- package/src/login.mjs +190 -0
- package/src/secrets.mjs +132 -0
- package/src/util.mjs +176 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Mnema Community License
|
|
2
|
+
|
|
3
|
+
_Version 1.0_
|
|
4
|
+
|
|
5
|
+
This Community License is based on the "Sustainable Use License" fair-code model.
|
|
6
|
+
It governs the **public core** of Mnema (the `mnema` repository). The enterprise
|
|
7
|
+
modules (`mnema-ee`) are covered by a separate proprietary license and are **not**
|
|
8
|
+
distributed under this license.
|
|
9
|
+
|
|
10
|
+
## Acceptance
|
|
11
|
+
|
|
12
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
13
|
+
|
|
14
|
+
## Copyright License
|
|
15
|
+
|
|
16
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable,
|
|
17
|
+
non-transferable license to use, copy, distribute, make available, and prepare
|
|
18
|
+
derivative works of the software, in each case subject to the limitations and
|
|
19
|
+
conditions below.
|
|
20
|
+
|
|
21
|
+
## Limitations
|
|
22
|
+
|
|
23
|
+
**Internal business purposes and personal use.** You may use the software for your
|
|
24
|
+
own internal business purposes or for personal use, and you may make the software
|
|
25
|
+
available to third parties **only** as part of a product or service you provide that
|
|
26
|
+
is not itself the software (or a modified version of it).
|
|
27
|
+
|
|
28
|
+
You may **not**, without a separate commercial agreement with the licensor:
|
|
29
|
+
|
|
30
|
+
1. **Host or offer the software as a service** to third parties (commercial hosting,
|
|
31
|
+
SaaS, or a multi-tenant deployment operated for others).
|
|
32
|
+
2. **Resell, sublicense, or redistribute the software as a product** whose primary
|
|
33
|
+
value is the software itself.
|
|
34
|
+
3. **Embed the software** into a commercial product offered to third parties where
|
|
35
|
+
the software provides the core functionality.
|
|
36
|
+
4. Remove or obscure any licensing, copyright, or other notices.
|
|
37
|
+
|
|
38
|
+
Using the software for **internal tooling, self-hosting for your own team or
|
|
39
|
+
organisation, evaluation, education, or development** is always permitted.
|
|
40
|
+
|
|
41
|
+
## Trademarks
|
|
42
|
+
|
|
43
|
+
This license does not grant you any right to use the licensor's trademarks, trade
|
|
44
|
+
names, or logos (including "Mnema"). Forks may use the code; they may not use the name.
|
|
45
|
+
|
|
46
|
+
## Patents
|
|
47
|
+
|
|
48
|
+
The licensor grants you a license, under any patent claims the licensor can license,
|
|
49
|
+
to make, use, sell, and import the software, limited to the same scope as the
|
|
50
|
+
copyright license above. This patent license terminates if you assert a patent claim
|
|
51
|
+
against anyone alleging the software infringes a patent.
|
|
52
|
+
|
|
53
|
+
## Notices
|
|
54
|
+
|
|
55
|
+
You must ensure that anyone who gets a copy of any part of the software from you also
|
|
56
|
+
gets a copy of these terms, and you must retain all copyright and licensing notices.
|
|
57
|
+
|
|
58
|
+
## No Other Rights
|
|
59
|
+
|
|
60
|
+
These terms do not imply any licenses other than those expressly granted here.
|
|
61
|
+
|
|
62
|
+
## Termination
|
|
63
|
+
|
|
64
|
+
If you use the software in violation of these terms, such use is not licensed, and
|
|
65
|
+
your licenses will automatically terminate. If the licensor provides you notice of a
|
|
66
|
+
violation and you cease the violation within 30 days, your licenses are reinstated.
|
|
67
|
+
|
|
68
|
+
## No Liability & As-Is
|
|
69
|
+
|
|
70
|
+
**As far as the law allows, the software comes "as is", without any warranty or
|
|
71
|
+
condition, and the licensor will not be liable to you for any damages arising out of
|
|
72
|
+
these terms or the use or nature of the software, under any kind of legal claim.**
|
|
73
|
+
|
|
74
|
+
## Definitions
|
|
75
|
+
|
|
76
|
+
The **licensor** is the entity offering these terms. The **software** is the software
|
|
77
|
+
the licensor makes available under these terms, including any portion of it. **You**
|
|
78
|
+
refers to the individual or entity agreeing to these terms.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
For commercial hosting, embedding, or reselling rights — or for the enterprise
|
|
83
|
+
(`mnema-ee`) modules — contact the licensor for a commercial agreement.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @mnemahq/cli
|
|
2
|
+
|
|
3
|
+
Connect a repository to your [Mnema](https://mnema.theboringpeople.in) workspace: install
|
|
4
|
+
Claude Code session capture (real token cost, git, files touched), backfill past sessions, and
|
|
5
|
+
search your workspace — all from the terminal.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @mnemahq/cli
|
|
11
|
+
mnema init
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The package is `@mnemahq/cli`; the command it installs is **`mnema`**. (The unscoped
|
|
15
|
+
name `mnema` is blocked on npm — its typosquat filter rejects it as too close to the
|
|
16
|
+
existing package `mem`.)
|
|
17
|
+
|
|
18
|
+
You'll be asked for your **workspace id** and a **hook token** (both in *Settings → Developer*), and
|
|
19
|
+
optionally an **API key** for search. `init` installs the capture hook, stores your secrets in the OS
|
|
20
|
+
keychain, and writes a `.mnema/config.json` (safe to commit — it holds no secrets).
|
|
21
|
+
|
|
22
|
+
Start a Claude Code session and it appears under **Sessions** with its cost.
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
| Command | What it does |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `mnema init` | Link the repo, install session capture, store secrets |
|
|
29
|
+
| `mnema status` | Connection, hook, API reachability, last session |
|
|
30
|
+
| `mnema sessions` | Recent sessions for this repo (local + server) |
|
|
31
|
+
| `mnema sweep` | Backfill past local sessions (opt-in, no history backfill) |
|
|
32
|
+
| `mnema pull` | Export repo-bound docs into `.mnema/context/**.md` (committed, offline-readable) |
|
|
33
|
+
| `mnema search "q"` | Search your workspace |
|
|
34
|
+
| `mnema doctor` | Diagnose install, hooks, auth, connectivity |
|
|
35
|
+
| `mnema uninstall` | Remove hooks and stored secrets (`--purge` also deletes `.mnema/`) |
|
|
36
|
+
|
|
37
|
+
## Secrets & privacy
|
|
38
|
+
|
|
39
|
+
Secrets are stored in the **OS keychain** (macOS Keychain, libsecret on Linux); if none is available
|
|
40
|
+
they fall back to a `0600` file under your config dir. A token is **never** written into the
|
|
41
|
+
committed `.mnema/config.json`. The capture hook sends metadata and file paths only — never file
|
|
42
|
+
contents. See the [privacy details](https://mnema.theboringpeople.in/docs/connect-agent).
|
|
43
|
+
|
|
44
|
+
`--origin <url>` (or `MNEMA_API_ORIGIN`) points the CLI at a self-hosted instance.
|
|
45
|
+
|
|
46
|
+
Requires Node 18+ and `git`.
|
|
47
|
+
|
|
48
|
+
## `.mnema/` repo artifacts
|
|
49
|
+
|
|
50
|
+
`mnema init` scaffolds a committed `.mnema/` directory and `mnema pull` fills it:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
.mnema/
|
|
54
|
+
config.json workspace + repo binding (no secrets)
|
|
55
|
+
NOTABILITY.md editable prose describing what's worth capturing in this repo
|
|
56
|
+
context/**.md workspace docs bound to this repo — readable offline, diff in PRs
|
|
57
|
+
manifest.json sync bookkeeping (hashes only)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Sync is server-is-truth, but your local edits are never silently overwritten.** On
|
|
61
|
+
`mnema pull`: an untouched file is updated from the server; a file you edited while the
|
|
62
|
+
server didn't change is kept as-is; if both changed, the server version is written beside
|
|
63
|
+
yours as `*.remote.md` (gitignored) and the conflict is reported.
|
package/bin/mnema.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mnemahq/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mnema CLI — connect a repo to your Mnema workspace: install session capture, sweep past sessions, and search from the terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mnema": "./bin/mnema.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mnema",
|
|
20
|
+
"cli",
|
|
21
|
+
"agent",
|
|
22
|
+
"claude-code",
|
|
23
|
+
"sessions"
|
|
24
|
+
],
|
|
25
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"vitest": "^3.2.7"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "node -e \"process.exit(0)\"",
|
|
34
|
+
"typecheck": "node --check bin/mnema.mjs && node --check src/cli.mjs && node --check src/util.mjs && node --check src/secrets.mjs",
|
|
35
|
+
"test": "vitest run"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .mnema/ repo artifacts (PR 5.4).
|
|
3
|
+
*
|
|
4
|
+
* `mnema pull` exports the workspace docs bound to this repo into
|
|
5
|
+
* `.mnema/context/**.md` — committed, human-readable, git-diffable, and readable
|
|
6
|
+
* by an agent with no network.
|
|
7
|
+
*
|
|
8
|
+
* Sync policy: SERVER IS TRUTH, but local edits are NEVER silently clobbered.
|
|
9
|
+
* - local file unchanged since last pull → overwrite with the server version
|
|
10
|
+
* - local edited, server unchanged → keep the local edit (local ahead)
|
|
11
|
+
* - both changed → CONFLICT: write the server version to
|
|
12
|
+
* `<file>.remote.md` (gitignored) and keep
|
|
13
|
+
* the local file; report it
|
|
14
|
+
* A manifest (`.mnema/manifest.json`, committed — hashes only, no content) records
|
|
15
|
+
* the server contentHash and the local hash at the last pull to detect all three.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { join, dirname } from 'node:path';
|
|
20
|
+
import {
|
|
21
|
+
existsSync, readFileSync, writeFileSync, mkdirSync,
|
|
22
|
+
} from 'node:fs';
|
|
23
|
+
|
|
24
|
+
const MANIFEST_VERSION = 1;
|
|
25
|
+
|
|
26
|
+
function sha256(s) { return createHash('sha256').update(s, 'utf8').digest('hex'); }
|
|
27
|
+
|
|
28
|
+
/** Sanitize a doc path into a safe relative file path under context/ (no traversal). */
|
|
29
|
+
export function targetRelPath(docPath) {
|
|
30
|
+
const segments = String(docPath || 'untitled')
|
|
31
|
+
.split('/')
|
|
32
|
+
.map((seg) => seg.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, ''))
|
|
33
|
+
.filter((seg) => seg && seg !== '..' && seg !== '.');
|
|
34
|
+
const rel = segments.join('/') || 'untitled';
|
|
35
|
+
return `context/${rel}.md`.replace(/\.md\.md$/, '.md');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mnemaDir(root) { return join(root, '.mnema'); }
|
|
39
|
+
function manifestPath(root) { return join(mnemaDir(root), 'manifest.json'); }
|
|
40
|
+
|
|
41
|
+
export function readManifest(root) {
|
|
42
|
+
try { return JSON.parse(readFileSync(manifestPath(root), 'utf8')); }
|
|
43
|
+
catch { return { version: MANIFEST_VERSION, files: {} }; }
|
|
44
|
+
}
|
|
45
|
+
function writeManifest(root, m) {
|
|
46
|
+
mkdirSync(mnemaDir(root), { recursive: true });
|
|
47
|
+
writeFileSync(manifestPath(root), JSON.stringify(m, null, 2) + '\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderFile(doc) {
|
|
51
|
+
const header = `<!-- Synced from Mnema · ${doc.path} · local edits are preserved and surfaced as conflicts on \`mnema pull\` -->\n\n`;
|
|
52
|
+
return header + (doc.markdown || '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Applies a set of remote docs to .mnema/context, honouring the sync policy.
|
|
57
|
+
* Returns a summary { written, updated, kept, conflicts, upToDate, orphaned }.
|
|
58
|
+
*/
|
|
59
|
+
export function applyContext(root, docs) {
|
|
60
|
+
const manifest = readManifest(root);
|
|
61
|
+
manifest.files = manifest.files || {};
|
|
62
|
+
const summary = { written: [], updated: [], kept: [], conflicts: [], upToDate: [], orphaned: [] };
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
|
|
65
|
+
for (const doc of docs) {
|
|
66
|
+
const rel = targetRelPath(doc.path);
|
|
67
|
+
seen.add(rel);
|
|
68
|
+
const abs = join(mnemaDir(root), rel);
|
|
69
|
+
const entry = manifest.files[rel];
|
|
70
|
+
const content = renderFile(doc);
|
|
71
|
+
const remoteHash = doc.contentHash;
|
|
72
|
+
|
|
73
|
+
if (!existsSync(abs)) {
|
|
74
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
75
|
+
writeFileSync(abs, content);
|
|
76
|
+
manifest.files[rel] = { docId: doc.docId, remoteHash, localHash: sha256(content), pulledAt: new Date().toISOString() };
|
|
77
|
+
summary.written.push(rel);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const localNow = sha256(readFileSync(abs, 'utf8'));
|
|
82
|
+
const localEdited = !entry || localNow !== entry.localHash;
|
|
83
|
+
const remoteChanged = !entry || remoteHash !== entry.remoteHash;
|
|
84
|
+
|
|
85
|
+
if (!localEdited) {
|
|
86
|
+
if (remoteChanged) {
|
|
87
|
+
writeFileSync(abs, content);
|
|
88
|
+
manifest.files[rel] = { docId: doc.docId, remoteHash, localHash: sha256(content), pulledAt: new Date().toISOString() };
|
|
89
|
+
summary.updated.push(rel);
|
|
90
|
+
} else {
|
|
91
|
+
summary.upToDate.push(rel);
|
|
92
|
+
}
|
|
93
|
+
} else if (!remoteChanged) {
|
|
94
|
+
// Local ahead of an unchanged server — keep the local edit untouched.
|
|
95
|
+
summary.kept.push(rel);
|
|
96
|
+
} else {
|
|
97
|
+
// Both changed — surface the server version beside the local file, keep local.
|
|
98
|
+
const sidecar = abs.replace(/\.md$/, '.remote.md');
|
|
99
|
+
mkdirSync(dirname(sidecar), { recursive: true });
|
|
100
|
+
writeFileSync(sidecar, content);
|
|
101
|
+
summary.conflicts.push(rel);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Docs that were pulled before but are no longer returned: leave the file in
|
|
106
|
+
// place (non-destructive) and just report it.
|
|
107
|
+
for (const rel of Object.keys(manifest.files)) {
|
|
108
|
+
if (!seen.has(rel)) summary.orphaned.push(rel);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
writeManifest(root, manifest);
|
|
112
|
+
return summary;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Scaffold the editable artifacts on init: NOTABILITY.md + .gitignore + context/. */
|
|
116
|
+
export function scaffold(root) {
|
|
117
|
+
const dir = mnemaDir(root);
|
|
118
|
+
mkdirSync(join(dir, 'context'), { recursive: true });
|
|
119
|
+
|
|
120
|
+
const gitignore = join(dir, '.gitignore');
|
|
121
|
+
if (!existsSync(gitignore)) {
|
|
122
|
+
writeFileSync(gitignore, '# Conflict sidecars from `mnema pull` — transient, do not commit\n*.remote.md\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const keep = join(dir, 'context', '.gitkeep');
|
|
126
|
+
if (!existsSync(keep)) writeFileSync(keep, '');
|
|
127
|
+
|
|
128
|
+
const notability = join(dir, 'NOTABILITY.md');
|
|
129
|
+
if (!existsSync(notability)) {
|
|
130
|
+
writeFileSync(notability, `# What's worth remembering in this repo
|
|
131
|
+
|
|
132
|
+
Edit this file in prose. Mnema consults it when deciding which decisions, gotchas
|
|
133
|
+
and invariants from a coding session are worth capturing as durable knowledge — so
|
|
134
|
+
tune it to your team's taste instead of a numeric threshold.
|
|
135
|
+
|
|
136
|
+
## Capture
|
|
137
|
+
|
|
138
|
+
- Non-obvious decisions and *why* an approach was chosen (or rejected)
|
|
139
|
+
- Gotchas that cost real time; invariants that must not be broken
|
|
140
|
+
- Anything a new engineer or agent would need to not repeat a mistake
|
|
141
|
+
|
|
142
|
+
## Skip
|
|
143
|
+
|
|
144
|
+
- Routine, self-evident changes; anything already obvious from the diff
|
|
145
|
+
- Secrets, credentials, or customer data
|
|
146
|
+
`);
|
|
147
|
+
}
|
|
148
|
+
}
|