memarium 0.13.1
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 +21 -0
- package/README.md +146 -0
- package/assets/scripts/merge-books.mjs +921 -0
- package/assets/workflows/memarium-aggregate.yml +66 -0
- package/dist/bin/memarium.js +6 -0
- package/dist/src/aggregated-store.js +95 -0
- package/dist/src/cli.js +175 -0
- package/dist/src/commands/cat.js +20 -0
- package/dist/src/commands/doctor.js +383 -0
- package/dist/src/commands/init-wizard.js +201 -0
- package/dist/src/commands/init.js +45 -0
- package/dist/src/commands/list.js +19 -0
- package/dist/src/commands/prune.js +108 -0
- package/dist/src/commands/resume/config-pathmap.js +38 -0
- package/dist/src/commands/resume/fuzzy-match.js +13 -0
- package/dist/src/commands/resume/list-sessions.js +54 -0
- package/dist/src/commands/resume/render-prompt.js +121 -0
- package/dist/src/commands/resume/resume.js +121 -0
- package/dist/src/commands/show.js +21 -0
- package/dist/src/commands/sync.js +279 -0
- package/dist/src/commands/upgrade.js +47 -0
- package/dist/src/commands/workflow.js +126 -0
- package/dist/src/config.js +98 -0
- package/dist/src/content-project-inference.js +185 -0
- package/dist/src/device.js +47 -0
- package/dist/src/digest/manifest.js +121 -0
- package/dist/src/digest/project-filter.js +32 -0
- package/dist/src/digest/session-signal.js +106 -0
- package/dist/src/digest/toc.js +127 -0
- package/dist/src/git-ops.js +359 -0
- package/dist/src/index-store.js +35 -0
- package/dist/src/migrate.js +72 -0
- package/dist/src/project-identity.js +139 -0
- package/dist/src/project-resolve.js +42 -0
- package/dist/src/prompts.js +87 -0
- package/dist/src/repo-data-dir.js +25 -0
- package/dist/src/slug.js +28 -0
- package/dist/src/sources/base.js +1 -0
- package/dist/src/sources/claude-code.js +294 -0
- package/dist/src/sources/vscode-copilot.js +400 -0
- package/dist/src/types.js +1 -0
- package/dist/src/writer.js +240 -0
- package/package.json +60 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Auto-generated by `memarium workflow init`. Edit freely once committed.
|
|
2
|
+
#
|
|
3
|
+
# Purpose: aggregate every device branch's book/ into main. The LLM work
|
|
4
|
+
# (chronicle/topics/cards writing) happens LOCALLY on each device via
|
|
5
|
+
# `memarium sync`. This workflow never calls an LLM — it just does a
|
|
6
|
+
# deterministic merge so `main` shows a unified view across all your devices.
|
|
7
|
+
#
|
|
8
|
+
# Triggers on push to ANY branch except main. The script is bundled in
|
|
9
|
+
# this repo at scripts/merge-books.mjs (written by `memarium workflow init`).
|
|
10
|
+
|
|
11
|
+
name: memarium aggregate book
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
branches-ignore:
|
|
16
|
+
- main
|
|
17
|
+
workflow_dispatch:
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: write # push merged book/ to main
|
|
21
|
+
|
|
22
|
+
concurrency:
|
|
23
|
+
group: memarium-aggregate
|
|
24
|
+
cancel-in-progress: false
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
aggregate:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
timeout-minutes: 10
|
|
30
|
+
steps:
|
|
31
|
+
- name: Checkout main (orphan if missing)
|
|
32
|
+
uses: actions/checkout@v4
|
|
33
|
+
with:
|
|
34
|
+
ref: main
|
|
35
|
+
fetch-depth: 0
|
|
36
|
+
continue-on-error: true
|
|
37
|
+
|
|
38
|
+
- name: Ensure main branch exists
|
|
39
|
+
run: |
|
|
40
|
+
if ! git rev-parse --verify main >/dev/null 2>&1; then
|
|
41
|
+
git checkout --orphan main
|
|
42
|
+
git rm -rf . 2>/dev/null || true
|
|
43
|
+
git commit --allow-empty -m "memarium: initialize main branch"
|
|
44
|
+
else
|
|
45
|
+
git checkout main
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
- name: Set up Node 20
|
|
49
|
+
uses: actions/setup-node@v4
|
|
50
|
+
with:
|
|
51
|
+
node-version: "20"
|
|
52
|
+
|
|
53
|
+
- name: Configure git identity
|
|
54
|
+
run: |
|
|
55
|
+
git config user.name "memarium-bot"
|
|
56
|
+
git config user.email "memarium-bot@users.noreply.github.com"
|
|
57
|
+
|
|
58
|
+
- name: Aggregate device books into main
|
|
59
|
+
env:
|
|
60
|
+
# Locale for rendered book pages. Substituted by `memarium workflow init`
|
|
61
|
+
# from the user's ~/.memarium/config.json `bookLocale` field. Default "en".
|
|
62
|
+
MEMARIUM_LOCALE: "__MEMARIUM_LOCALE__"
|
|
63
|
+
run: node scripts/merge-books.mjs
|
|
64
|
+
|
|
65
|
+
- name: Push main
|
|
66
|
+
run: git push origin HEAD:main
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { simpleGit } from "simple-git";
|
|
5
|
+
/** Per-clone read-only mirror of `origin/main` mounted as a separate git
|
|
6
|
+
* worktree. Holds the union of every device's raw_sessions/ plus
|
|
7
|
+
* `.memarium/index.aggregated.json` (written by CI's merge-books.mjs).
|
|
8
|
+
*
|
|
9
|
+
* P7 (0.8.0): without this, `memarium resume <id>` could only find sessions
|
|
10
|
+
* recorded by THIS device — sessions captured on a sibling device were
|
|
11
|
+
* unreachable until you manually checked out the other device branch.
|
|
12
|
+
*
|
|
13
|
+
* Layout: `<HOME>/.memarium/aggregated/` — sibling to `session-repo/`.
|
|
14
|
+
* Both worktrees share the same `.git` database. */
|
|
15
|
+
export function aggregatedPath() {
|
|
16
|
+
return join(homedir(), ".memarium", "aggregated");
|
|
17
|
+
}
|
|
18
|
+
/** Path inside aggregated worktree where merge-books.mjs writes the union
|
|
19
|
+
* index. Absent when CI hasn't run yet or no device had spool data. */
|
|
20
|
+
export function aggregatedIndexAbs() {
|
|
21
|
+
return join(aggregatedPath(), ".memarium", "index.aggregated.json");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Ensure the aggregated worktree exists and points at the latest origin/main.
|
|
25
|
+
* Best-effort — returns true on success, false otherwise (caller logs and
|
|
26
|
+
* proceeds; aggregation is opt-in eye candy, not a hard requirement).
|
|
27
|
+
*
|
|
28
|
+
* First call: `git worktree add <aggPath> main` from the session-repo.
|
|
29
|
+
* Subsequent calls: `git -C <aggPath> fetch + reset --hard origin/main`.
|
|
30
|
+
* (Use reset instead of pull --ff-only because CI rewrites raw_sessions/
|
|
31
|
+
* with every aggregate commit; a pure ff is the common case but reset
|
|
32
|
+
* keeps the worktree clean even when the user accidentally edits files
|
|
33
|
+
* inside it.)
|
|
34
|
+
*/
|
|
35
|
+
export async function refreshAggregatedWorktree(sessionRepoPath) {
|
|
36
|
+
const aggPath = aggregatedPath();
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(join(aggPath, ".git"))) {
|
|
39
|
+
// First-time setup. `git worktree add` from the session repo, pointing
|
|
40
|
+
// at the local-tracking `main` ref. If `main` doesn't exist locally yet
|
|
41
|
+
// (very fresh clone), fall back to creating it from origin/main.
|
|
42
|
+
mkdirSync(dirname(aggPath), { recursive: true });
|
|
43
|
+
const repoGit = simpleGit(sessionRepoPath);
|
|
44
|
+
// Make sure we have origin/main locally first.
|
|
45
|
+
await repoGit.fetch("origin", "main").catch(() => { });
|
|
46
|
+
// Ensure a local `main` ref tracks origin/main so worktree add can
|
|
47
|
+
// point at a name (worktrees can't share a HEAD with another worktree,
|
|
48
|
+
// and the session-repo is usually on a device branch already).
|
|
49
|
+
const localMain = await repoGit.raw(["branch", "--list", "main"]).catch(() => "");
|
|
50
|
+
if (!localMain.trim()) {
|
|
51
|
+
await repoGit.raw(["branch", "main", "origin/main"]).catch(() => { });
|
|
52
|
+
}
|
|
53
|
+
await repoGit.raw(["worktree", "add", aggPath, "main"]);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
// Refresh path. Worktree exists; fetch + reset --hard to origin/main.
|
|
57
|
+
const aggGit = simpleGit(aggPath);
|
|
58
|
+
await aggGit.fetch("origin", "main");
|
|
59
|
+
await aggGit.raw(["reset", "--hard", "origin/main"]);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Read the aggregated index from the worktree. Returns null when:
|
|
67
|
+
* - the worktree doesn't exist (first sync hasn't called refresh)
|
|
68
|
+
* - main has never received an aggregate commit (no devices ran sync yet)
|
|
69
|
+
* - the file is malformed.
|
|
70
|
+
* Callers should treat null as "no cross-device sessions known" and proceed
|
|
71
|
+
* with own-only data. */
|
|
72
|
+
export function loadAggregatedIndex() {
|
|
73
|
+
const p = aggregatedIndexAbs();
|
|
74
|
+
if (!existsSync(p))
|
|
75
|
+
return null;
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(readFileSync(p, "utf8"));
|
|
78
|
+
if (parsed.version !== 1 || !parsed.entries)
|
|
79
|
+
return null;
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Resolve the absolute path of a session's md file. Sessions captured by
|
|
87
|
+
* THIS device live under the session-repo working tree; sessions from
|
|
88
|
+
* sibling devices (= entries with originDevice set, only present in the
|
|
89
|
+
* aggregated index) live under the aggregated worktree. The IndexEntry's
|
|
90
|
+
* `relativePath` is the same in both cases — just resolved against a
|
|
91
|
+
* different root. */
|
|
92
|
+
export function resolveSessionMdPath(sessionRepoPath, entry, isAggregated) {
|
|
93
|
+
const root = isAggregated ? aggregatedPath() : sessionRepoPath;
|
|
94
|
+
return join(root, entry.relativePath);
|
|
95
|
+
}
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
/** Read version straight from the bundled package.json so we can never
|
|
7
|
+
* ship a CLI whose --version lies. Two layouts to handle:
|
|
8
|
+
* dev: src/cli.ts → ../package.json
|
|
9
|
+
* built: dist/src/cli.js → ../../package.json */
|
|
10
|
+
function readPackageVersion() {
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(resolve(here, rel), "utf8")).version;
|
|
15
|
+
}
|
|
16
|
+
catch { /* try next */ }
|
|
17
|
+
}
|
|
18
|
+
return "0.0.0-unknown";
|
|
19
|
+
}
|
|
20
|
+
export async function run(argv) {
|
|
21
|
+
const program = new Command();
|
|
22
|
+
program
|
|
23
|
+
.name("memarium")
|
|
24
|
+
.description("Vibe coding memory book")
|
|
25
|
+
// Standard CLI convention: -v + --version. Commander defaults to -V
|
|
26
|
+
// (uppercase) which most users don't reach for; we override to lowercase.
|
|
27
|
+
.version(readPackageVersion(), "-v, --version", "print the installed memarium version");
|
|
28
|
+
program
|
|
29
|
+
.command("init [repoUrl]")
|
|
30
|
+
.description("Initialize memarium. Run with no arguments for the interactive wizard, or pass a repoUrl + flags for non-interactive setup.")
|
|
31
|
+
.option("--local-path <path>", "local checkout path (default ./.memarium/repo)")
|
|
32
|
+
.option("--no-digest", "skip the digest pipeline (raw push only)")
|
|
33
|
+
.option("--device <name>", "device branch name (default: sanitized os.hostname())")
|
|
34
|
+
.action(async (repoUrl, opts) => {
|
|
35
|
+
const { initCmd } = await import("./commands/init.js");
|
|
36
|
+
await initCmd({
|
|
37
|
+
repoUrl,
|
|
38
|
+
localPath: opts.localPath,
|
|
39
|
+
digestEnabled: opts.digest !== false,
|
|
40
|
+
device: opts.device,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
program
|
|
44
|
+
.command("sync")
|
|
45
|
+
.description("Extract sessions from local Claude Code + VS Code Copilot Chat, commit + push to your device branch. No LLM call. Run /memarium in Claude Code afterward to digest.")
|
|
46
|
+
.action(async () => {
|
|
47
|
+
const { syncCmd } = await import("./commands/sync.js");
|
|
48
|
+
await syncCmd();
|
|
49
|
+
});
|
|
50
|
+
program
|
|
51
|
+
.command("upgrade")
|
|
52
|
+
.description("Refresh the npm CLI (`npm install -g memarium@latest`). Skips the npm step if memarium is npm-link'd from a dev checkout. To update the Claude Code plugin, run `/plugin update memarium` in any session.")
|
|
53
|
+
.option("--no-cli", "skip the `npm install -g` step")
|
|
54
|
+
.action(async (opts) => {
|
|
55
|
+
const { upgradeCmd } = await import("./commands/upgrade.js");
|
|
56
|
+
await upgradeCmd({ noCli: opts.cli === false });
|
|
57
|
+
});
|
|
58
|
+
program
|
|
59
|
+
.command("doctor")
|
|
60
|
+
.description("Health check: CLI version on PATH, npm latest, Claude plugin manifest + install entry, ~/.memarium/config presence, memex availability. Read-only and offline-tolerant.")
|
|
61
|
+
.action(async () => {
|
|
62
|
+
const { doctorCmd } = await import("./commands/doctor.js");
|
|
63
|
+
await doctorCmd();
|
|
64
|
+
});
|
|
65
|
+
program
|
|
66
|
+
.command("prune")
|
|
67
|
+
.description("Find raw_sessions/*.md files on disk that are not referenced by .memarium/index.json (orphans from earlier extractor bugs) and optionally delete them. Dry-run by default.")
|
|
68
|
+
.option("--apply", "actually delete the orphan files (default is dry-run)")
|
|
69
|
+
.action(async (opts) => {
|
|
70
|
+
const { pruneCmd } = await import("./commands/prune.js");
|
|
71
|
+
await pruneCmd({ apply: opts.apply });
|
|
72
|
+
});
|
|
73
|
+
program
|
|
74
|
+
.command("workflow")
|
|
75
|
+
.description("Manage the GitHub Action that aggregates device branches into main")
|
|
76
|
+
.addCommand(new Command("init")
|
|
77
|
+
.description("Write .github/workflows/memarium-aggregate.yml + scripts/merge-books.mjs into the configured memarium repo, then commit + push")
|
|
78
|
+
.option("--force", "overwrite if files already exist")
|
|
79
|
+
.option("--no-push", "write the files locally but don't auto commit + push")
|
|
80
|
+
.action(async (opts) => {
|
|
81
|
+
const { workflowInitCmd } = await import("./commands/workflow.js");
|
|
82
|
+
// commander's --no-X sets opts.X=false when flag present, true otherwise.
|
|
83
|
+
await workflowInitCmd({ force: opts.force, noPush: opts.push === false });
|
|
84
|
+
}));
|
|
85
|
+
program
|
|
86
|
+
.command("list")
|
|
87
|
+
.description("List synced sessions")
|
|
88
|
+
.option("--tool <name>", "filter by claude|copilot")
|
|
89
|
+
.option("--project <name>", "filter by project")
|
|
90
|
+
.action(async (opts) => {
|
|
91
|
+
const { listCmd } = await import("./commands/list.js");
|
|
92
|
+
await listCmd(opts);
|
|
93
|
+
});
|
|
94
|
+
program
|
|
95
|
+
.command("show <ref>")
|
|
96
|
+
.description("Show a session by sessionId, shortId, slug, or display name")
|
|
97
|
+
.action(async (ref) => {
|
|
98
|
+
const { showCmd } = await import("./commands/show.js");
|
|
99
|
+
await showCmd(ref);
|
|
100
|
+
});
|
|
101
|
+
program
|
|
102
|
+
.command("cat <path>")
|
|
103
|
+
.description("Print a repo file to stdout. Path is absolute or relative to the configured repoPath. Used by the /memarium skill to read session md.")
|
|
104
|
+
.action(async (path) => {
|
|
105
|
+
const { catCmd } = await import("./commands/cat.js");
|
|
106
|
+
await catCmd(path);
|
|
107
|
+
});
|
|
108
|
+
program
|
|
109
|
+
.command("list-sessions")
|
|
110
|
+
.description("List sessions in the local spool, filterable for cross-device resume.")
|
|
111
|
+
.option("--project <slug>", "filter by project slug")
|
|
112
|
+
.option("--since <window>", "only sessions ended within this window (e.g. 7d, 24h, 4w)")
|
|
113
|
+
.option("--device <name>", "filter by device branch (placeholder for v0.6)")
|
|
114
|
+
.action(async (opts) => {
|
|
115
|
+
const { listSessionsCmd } = await import("./commands/resume/list-sessions.js");
|
|
116
|
+
const sessions = await listSessionsCmd(opts);
|
|
117
|
+
if (sessions.length === 0) {
|
|
118
|
+
console.log("(no sessions match)");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const rows = sessions.map((e) => ({
|
|
122
|
+
SESSION: e.shortId,
|
|
123
|
+
TOOL: e.tool,
|
|
124
|
+
PROJECT: e.project,
|
|
125
|
+
ENDED: e.endedAt.slice(0, 10),
|
|
126
|
+
TITLE: e.displayName.slice(0, 40),
|
|
127
|
+
}));
|
|
128
|
+
console.table(rows);
|
|
129
|
+
});
|
|
130
|
+
program
|
|
131
|
+
.command("resume <id>")
|
|
132
|
+
.description("Find a session's context.md and spawn a fresh `claude` session with it as the first prompt.")
|
|
133
|
+
.option("--print", "print the claude invocation instead of spawning it")
|
|
134
|
+
.option("--cwd <path>", "override the cwd used for project-match validation (default: process.cwd())")
|
|
135
|
+
.action(async (id, opts) => {
|
|
136
|
+
const { resumeCmd } = await import("./commands/resume/resume.js");
|
|
137
|
+
try {
|
|
138
|
+
await resumeCmd({ idOrPrefix: id, print: opts.print, cwd: opts.cwd });
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.error(chalk.red(String(err.message)));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
program
|
|
146
|
+
.command("config")
|
|
147
|
+
.description("Inspect or modify ~/.memarium/config.json.")
|
|
148
|
+
.option("--map-path <FROM=TO>", "add a cross-device path mapping (e.g. /Users/yueA=/Users/yueB) for `memarium resume`")
|
|
149
|
+
.option("--device <name>", "set a stable device branch name (e.g. 'mini2') — overrides the volatile hostname() default")
|
|
150
|
+
.action(async (opts) => {
|
|
151
|
+
if (opts.mapPath) {
|
|
152
|
+
const { setMapPath } = await import("./commands/resume/config-pathmap.js");
|
|
153
|
+
setMapPath(opts.mapPath);
|
|
154
|
+
console.log(`Added pathMap entry. Current ~/.memarium/config.json updated.`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (opts.device) {
|
|
158
|
+
const { setDeviceBranch } = await import("./commands/resume/config-pathmap.js");
|
|
159
|
+
const { previous, current } = setDeviceBranch(opts.device);
|
|
160
|
+
console.log(`Device branch: ${previous} → ${current}`);
|
|
161
|
+
if (previous && previous !== current) {
|
|
162
|
+
console.log(`\nNote: your spool repo may still have a local + remote branch named '${previous}'.\n` +
|
|
163
|
+
`If you want to clean those up:\n` +
|
|
164
|
+
` cd ~/.memarium/session-repo\n` +
|
|
165
|
+
` git branch -D '${previous}' 2>/dev/null\n` +
|
|
166
|
+
` git push origin --delete '${previous}' 2>/dev/null`);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// No flags: print current config
|
|
171
|
+
const { readConfig } = await import("./config.js");
|
|
172
|
+
console.log(JSON.stringify(readConfig(), null, 2));
|
|
173
|
+
});
|
|
174
|
+
await program.parseAsync(argv);
|
|
175
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { readConfig } from "../config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Dump a session file (or any repo file) to stdout. Used by the in-session
|
|
6
|
+
* /memarium skill to read session md.
|
|
7
|
+
*
|
|
8
|
+
* Path resolution:
|
|
9
|
+
* - absolute path → used as-is
|
|
10
|
+
* - relative path → resolved against `cfg.repoPath`
|
|
11
|
+
*/
|
|
12
|
+
export async function catCmd(path) {
|
|
13
|
+
if (!path)
|
|
14
|
+
throw new Error("usage: memarium cat <path>");
|
|
15
|
+
const cfg = readConfig();
|
|
16
|
+
const abs = isAbsolute(path) ? path : join(cfg.repoPath, path);
|
|
17
|
+
if (!existsSync(abs))
|
|
18
|
+
throw new Error(`not found: ${abs}`);
|
|
19
|
+
process.stdout.write(readFileSync(abs));
|
|
20
|
+
}
|