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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/assets/scripts/merge-books.mjs +921 -0
  4. package/assets/workflows/memarium-aggregate.yml +66 -0
  5. package/dist/bin/memarium.js +6 -0
  6. package/dist/src/aggregated-store.js +95 -0
  7. package/dist/src/cli.js +175 -0
  8. package/dist/src/commands/cat.js +20 -0
  9. package/dist/src/commands/doctor.js +383 -0
  10. package/dist/src/commands/init-wizard.js +201 -0
  11. package/dist/src/commands/init.js +45 -0
  12. package/dist/src/commands/list.js +19 -0
  13. package/dist/src/commands/prune.js +108 -0
  14. package/dist/src/commands/resume/config-pathmap.js +38 -0
  15. package/dist/src/commands/resume/fuzzy-match.js +13 -0
  16. package/dist/src/commands/resume/list-sessions.js +54 -0
  17. package/dist/src/commands/resume/render-prompt.js +121 -0
  18. package/dist/src/commands/resume/resume.js +121 -0
  19. package/dist/src/commands/show.js +21 -0
  20. package/dist/src/commands/sync.js +279 -0
  21. package/dist/src/commands/upgrade.js +47 -0
  22. package/dist/src/commands/workflow.js +126 -0
  23. package/dist/src/config.js +98 -0
  24. package/dist/src/content-project-inference.js +185 -0
  25. package/dist/src/device.js +47 -0
  26. package/dist/src/digest/manifest.js +121 -0
  27. package/dist/src/digest/project-filter.js +32 -0
  28. package/dist/src/digest/session-signal.js +106 -0
  29. package/dist/src/digest/toc.js +127 -0
  30. package/dist/src/git-ops.js +359 -0
  31. package/dist/src/index-store.js +35 -0
  32. package/dist/src/migrate.js +72 -0
  33. package/dist/src/project-identity.js +139 -0
  34. package/dist/src/project-resolve.js +42 -0
  35. package/dist/src/prompts.js +87 -0
  36. package/dist/src/repo-data-dir.js +25 -0
  37. package/dist/src/slug.js +28 -0
  38. package/dist/src/sources/base.js +1 -0
  39. package/dist/src/sources/claude-code.js +294 -0
  40. package/dist/src/sources/vscode-copilot.js +400 -0
  41. package/dist/src/types.js +1 -0
  42. package/dist/src/writer.js +240 -0
  43. 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,6 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "../src/cli.js";
3
+ run(process.argv).catch((e) => {
4
+ console.error(e instanceof Error ? e.message : e);
5
+ process.exit(1);
6
+ });
@@ -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
+ }
@@ -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
+ }