claude-git-sessions 0.1.2 → 0.2.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/README.md +25 -0
- package/dist/commands/memory.js +185 -0
- package/dist/constants.js +7 -0
- package/dist/git.js +7 -3
- package/dist/index.js +39 -1
- package/dist/memory.js +134 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,6 +45,8 @@ history with `main` and never contains your source files — only session data:
|
|
|
45
45
|
```
|
|
46
46
|
sessions/<session-id>.jsonl # the transcript, verbatim
|
|
47
47
|
sessions/<session-id>.meta.json # sidecar metadata (name, author, cwd, …)
|
|
48
|
+
memory/<fact>.md # shared memory facts (see `ccgs memory`)
|
|
49
|
+
memory/<fact>.md.meta.json # sidecar metadata (type, author, …)
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
Files are keyed by the globally-unique Claude Code session UUID, so transcripts
|
|
@@ -101,6 +103,29 @@ Shows what will be deleted and asks for `y/N` confirmation (`--yes`/`-y` to skip
|
|
|
101
103
|
for scripting). Removes both files from the branch and pushes. By default only
|
|
102
104
|
the shared branch is touched; add `--local` to also remove the local copy.
|
|
103
105
|
|
|
106
|
+
### `ccgs memory push [--all]` / `ccgs memory pull [--all] [--force]`
|
|
107
|
+
|
|
108
|
+
Claude Code keeps per-project memory in `~/.claude/projects/<slug>/memory/` —
|
|
109
|
+
one Markdown file per fact, plus a `MEMORY.md` index. `ccgs memory` shares those
|
|
110
|
+
facts on the same orphan branch (under a `memory/` prefix).
|
|
111
|
+
|
|
112
|
+
Memory is *mixed-sensitivity*, so it's filtered by the fact's frontmatter
|
|
113
|
+
`type`:
|
|
114
|
+
|
|
115
|
+
- **`project` / `reference`** facts ("how we deploy", "where the API docs live")
|
|
116
|
+
are team knowledge and are shared **by default**.
|
|
117
|
+
- **`user` / `feedback`** facts (personal preferences) are held back unless you
|
|
118
|
+
pass **`--all`**.
|
|
119
|
+
|
|
120
|
+
`pull` writes the shared facts into your local memory dir and maintains a
|
|
121
|
+
clearly-marked block inside your `MEMORY.md` pointing at them — **your own index
|
|
122
|
+
lines are left untouched**. `MEMORY.md` itself is never pushed as a blob (two
|
|
123
|
+
people's indexes would clobber each other). Facts are keyed by filename with
|
|
124
|
+
newer-wins conflict handling (`--force` to overwrite a newer local copy).
|
|
125
|
+
|
|
126
|
+
> Scope: only the repo **root** slug's memory dir is synced (the common case);
|
|
127
|
+
> memory from sessions started in subdirectories is not.
|
|
128
|
+
|
|
104
129
|
### Global options
|
|
105
130
|
|
|
106
131
|
| Option | Default | Meaning |
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { MEMORY_DIR, MEMORY_INDEX } from "../constants.js";
|
|
5
|
+
import { applyToOrphanBranch, assertRemote, fetchBranch, localTrackingRef, lsTreeUnder, showFile, validateBranchName, } from "../git.js";
|
|
6
|
+
import { gitAuthor } from "../meta.js";
|
|
7
|
+
import { indexLine, isShareable, memoryDir, parseFrontmatter, readLocalMemory, titleFor, updateIndexBlock, } from "../memory.js";
|
|
8
|
+
function metaPath(filename) {
|
|
9
|
+
return `${MEMORY_DIR}/${filename}.meta.json`;
|
|
10
|
+
}
|
|
11
|
+
function blobPath(filename) {
|
|
12
|
+
return `${MEMORY_DIR}/${filename}`;
|
|
13
|
+
}
|
|
14
|
+
/** Filenames of `.md` memory facts on the branch (excludes sidecars). */
|
|
15
|
+
async function listBranchMemory(repo, ref) {
|
|
16
|
+
const paths = await lsTreeUnder(repo, ref, MEMORY_DIR);
|
|
17
|
+
const names = new Set();
|
|
18
|
+
for (const p of paths) {
|
|
19
|
+
const m = p.match(new RegExp(`^${MEMORY_DIR}/(.+\\.md)$`));
|
|
20
|
+
if (m && m[1] !== MEMORY_INDEX)
|
|
21
|
+
names.add(m[1]);
|
|
22
|
+
}
|
|
23
|
+
return [...names];
|
|
24
|
+
}
|
|
25
|
+
export async function memoryPush(opts) {
|
|
26
|
+
await validateBranchName(opts.repoRoot, opts.branch);
|
|
27
|
+
await assertRemote(opts.repoRoot, opts.remote);
|
|
28
|
+
const all = readLocalMemory(opts.repoRoot);
|
|
29
|
+
const shareable = all.filter((f) => isShareable(f.type, opts.all));
|
|
30
|
+
const heldBack = all.length - shareable.length;
|
|
31
|
+
if (shareable.length === 0) {
|
|
32
|
+
const extra = heldBack
|
|
33
|
+
? ` (${heldBack} personal user/feedback memo(ies) held back; use --all to include)`
|
|
34
|
+
: "";
|
|
35
|
+
console.log(`nothing to push (no shareable memory found for this repo)${extra}`);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
await fetchBranch(opts.repoRoot, opts.remote, opts.branch);
|
|
39
|
+
const ref = localTrackingRef(opts.branch);
|
|
40
|
+
const existing = new Set(await listBranchMemory(opts.repoRoot, ref).catch(() => []));
|
|
41
|
+
const author = await gitAuthor(opts.repoRoot);
|
|
42
|
+
const added = [];
|
|
43
|
+
const updated = [];
|
|
44
|
+
let unchanged = 0;
|
|
45
|
+
for (const f of shareable) {
|
|
46
|
+
if (!existing.has(f.filename)) {
|
|
47
|
+
added.push(f.filename);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const prev = await showFile(opts.repoRoot, ref, blobPath(f.filename));
|
|
51
|
+
if (prev === f.content)
|
|
52
|
+
unchanged++;
|
|
53
|
+
else
|
|
54
|
+
updated.push(f.filename);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const result = await applyToOrphanBranch(opts.repoRoot, opts.remote, opts.branch, `ccgs: push ${added.length + updated.length} memory file(s)`, async (b) => {
|
|
58
|
+
for (const f of shareable) {
|
|
59
|
+
const sidecar = {
|
|
60
|
+
filename: f.filename,
|
|
61
|
+
type: f.type,
|
|
62
|
+
name: f.name,
|
|
63
|
+
description: f.description,
|
|
64
|
+
author,
|
|
65
|
+
machine: os.hostname(),
|
|
66
|
+
updatedAt: f.updatedAt,
|
|
67
|
+
};
|
|
68
|
+
await b.addBlob(blobPath(f.filename), f.content);
|
|
69
|
+
await b.addBlob(metaPath(f.filename), JSON.stringify(sidecar, null, 2) + "\n");
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
if (!result.changed) {
|
|
73
|
+
console.log(`Memory already up to date — ${unchanged} file(s) unchanged.`);
|
|
74
|
+
if (heldBack)
|
|
75
|
+
console.log(`(${heldBack} personal memo(ies) held back; --all to include)`);
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
if (added.length) {
|
|
79
|
+
console.log(`Added ${added.length}:`);
|
|
80
|
+
for (const a of added)
|
|
81
|
+
console.log(` + ${a}`);
|
|
82
|
+
}
|
|
83
|
+
if (updated.length) {
|
|
84
|
+
console.log(`Updated ${updated.length}:`);
|
|
85
|
+
for (const u of updated)
|
|
86
|
+
console.log(` ~ ${u}`);
|
|
87
|
+
}
|
|
88
|
+
if (unchanged)
|
|
89
|
+
console.log(`(${unchanged} unchanged)`);
|
|
90
|
+
if (heldBack)
|
|
91
|
+
console.log(`(${heldBack} personal user/feedback memo(ies) held back; --all to include)`);
|
|
92
|
+
console.log(`Pushed memory to ${opts.remote} @ccgs/${opts.branch}.`);
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
export async function memoryPull(opts) {
|
|
96
|
+
await validateBranchName(opts.repoRoot, opts.branch);
|
|
97
|
+
await assertRemote(opts.repoRoot, opts.remote);
|
|
98
|
+
const exists = await fetchBranch(opts.repoRoot, opts.remote, opts.branch);
|
|
99
|
+
if (!exists) {
|
|
100
|
+
console.log("nothing to pull (branch does not exist on remote)");
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
const ref = localTrackingRef(opts.branch);
|
|
104
|
+
const filenames = await listBranchMemory(opts.repoRoot, ref);
|
|
105
|
+
if (filenames.length === 0) {
|
|
106
|
+
console.log("nothing to pull (no shared memory on branch)");
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
const dir = memoryDir(opts.repoRoot);
|
|
110
|
+
const pulled = [];
|
|
111
|
+
const skipped = [];
|
|
112
|
+
// Files present locally after this pull, for index regeneration.
|
|
113
|
+
const indexEntries = [];
|
|
114
|
+
for (const filename of filenames) {
|
|
115
|
+
const content = await showFile(opts.repoRoot, ref, blobPath(filename));
|
|
116
|
+
if (content === null) {
|
|
117
|
+
skipped.push(`${filename}: missing blob`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const rawMeta = await showFile(opts.repoRoot, ref, metaPath(filename));
|
|
121
|
+
let sidecar = null;
|
|
122
|
+
if (rawMeta) {
|
|
123
|
+
try {
|
|
124
|
+
sidecar = JSON.parse(rawMeta);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
sidecar = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const type = sidecar?.type ?? parseFrontmatter(content).type;
|
|
131
|
+
// Apply the same type gate on pull: don't litter local memory with other
|
|
132
|
+
// people's personal facts unless explicitly asked.
|
|
133
|
+
if (!isShareable(type, opts.all)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const description = sidecar?.description ?? parseFrontmatter(content).description;
|
|
137
|
+
const localFile = path.join(dir, filename);
|
|
138
|
+
// Conflict policy: keep a newer local copy unless --force.
|
|
139
|
+
if (!opts.force && fs.existsSync(localFile)) {
|
|
140
|
+
const branchUpdated = sidecar?.updatedAt ?? "";
|
|
141
|
+
const localUpdated = fs.statSync(localFile).mtime.toISOString();
|
|
142
|
+
if (branchUpdated && localUpdated > branchUpdated) {
|
|
143
|
+
skipped.push(`${filename}: local copy is newer (use --force)`);
|
|
144
|
+
// Still index it — the file exists locally and is part of the shared set.
|
|
145
|
+
indexEntries.push({ filename, description, title: titleFor({ filename, name: sidecar?.name ?? null }) });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
150
|
+
fs.writeFileSync(localFile, content);
|
|
151
|
+
pulled.push(filename);
|
|
152
|
+
indexEntries.push({ filename, description, title: titleFor({ filename, name: sidecar?.name ?? null }) });
|
|
153
|
+
}
|
|
154
|
+
// Rebuild the ccgs-managed block in MEMORY.md from the shared set.
|
|
155
|
+
if (indexEntries.length) {
|
|
156
|
+
const indexPath = path.join(dir, MEMORY_INDEX);
|
|
157
|
+
let existing = "";
|
|
158
|
+
try {
|
|
159
|
+
existing = fs.readFileSync(indexPath, "utf8");
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
existing = "";
|
|
163
|
+
}
|
|
164
|
+
const lines = indexEntries
|
|
165
|
+
.sort((a, b) => a.filename.localeCompare(b.filename))
|
|
166
|
+
.map((e) => indexLine(e));
|
|
167
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
168
|
+
fs.writeFileSync(indexPath, updateIndexBlock(existing, lines));
|
|
169
|
+
}
|
|
170
|
+
if (pulled.length) {
|
|
171
|
+
console.log(`Pulled ${pulled.length} memory file(s):`);
|
|
172
|
+
for (const p of pulled)
|
|
173
|
+
console.log(` + ${p}`);
|
|
174
|
+
console.log(`Index updated: ${path.join(dir, MEMORY_INDEX)}`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log("Pulled 0 memory files.");
|
|
178
|
+
}
|
|
179
|
+
if (skipped.length) {
|
|
180
|
+
console.log(`Skipped ${skipped.length}:`);
|
|
181
|
+
for (const s of skipped)
|
|
182
|
+
console.log(` - ${s}`);
|
|
183
|
+
}
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
package/dist/constants.js
CHANGED
|
@@ -15,5 +15,12 @@ export const DEFAULT_BRANCH_NAME = "default";
|
|
|
15
15
|
export const DEFAULT_REMOTE = "origin";
|
|
16
16
|
/** Path prefix used for session blobs on the orphan branch. */
|
|
17
17
|
export const SESSIONS_DIR = "sessions";
|
|
18
|
+
/**
|
|
19
|
+
* Path prefix used for shared memory blobs on the orphan branch, and the name
|
|
20
|
+
* of the local memory subdirectory inside a project's slug dir.
|
|
21
|
+
*/
|
|
22
|
+
export const MEMORY_DIR = "memory";
|
|
23
|
+
/** The hand-maintained memory index file (lives inside the memory dir). */
|
|
24
|
+
export const MEMORY_INDEX = "MEMORY.md";
|
|
18
25
|
/** Max length of an auto-derived display name. */
|
|
19
26
|
export const DISPLAY_NAME_MAX = 80;
|
package/dist/git.js
CHANGED
|
@@ -115,15 +115,19 @@ export async function trackingTip(repo, name) {
|
|
|
115
115
|
const sha = r.stdout.trim();
|
|
116
116
|
return sha || null;
|
|
117
117
|
}
|
|
118
|
-
/** List `
|
|
119
|
-
export async function
|
|
118
|
+
/** List paths under `prefix/` present on a tree-ish ref. */
|
|
119
|
+
export async function lsTreeUnder(repo, ref, prefix) {
|
|
120
120
|
const r = await git(["ls-tree", "-r", "--name-only", ref], { cwd: repo, allowFail: true });
|
|
121
121
|
if (r.code !== 0)
|
|
122
122
|
return [];
|
|
123
123
|
return r.stdout
|
|
124
124
|
.split("\n")
|
|
125
125
|
.map((s) => s.trim())
|
|
126
|
-
.filter((p) => p.startsWith(`${
|
|
126
|
+
.filter((p) => p.startsWith(`${prefix}/`));
|
|
127
|
+
}
|
|
128
|
+
/** List `sessions/*` paths present on a tree-ish ref. */
|
|
129
|
+
export async function lsSessions(repo, ref) {
|
|
130
|
+
return lsTreeUnder(repo, ref, SESSIONS_DIR);
|
|
127
131
|
}
|
|
128
132
|
/** Read a blob at `ref:path` as utf-8 text, or null if it does not exist. */
|
|
129
133
|
export async function showFile(repo, ref, filePath) {
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,10 @@ import { repoRoot } from "./git.js";
|
|
|
5
5
|
import { pull } from "./commands/pull.js";
|
|
6
6
|
import { push } from "./commands/push.js";
|
|
7
7
|
import { deleteSession } from "./commands/delete.js";
|
|
8
|
+
import { memoryPull, memoryPush } from "./commands/memory.js";
|
|
8
9
|
// Kept in sync with package.json at publish time; hardcoded to avoid a JSON
|
|
9
10
|
// import assertion just for --version.
|
|
10
|
-
const VERSION = "0.
|
|
11
|
+
const VERSION = "0.2.0";
|
|
11
12
|
function globals(cmd) {
|
|
12
13
|
const opts = cmd.optsWithGlobals();
|
|
13
14
|
return {
|
|
@@ -73,6 +74,43 @@ program
|
|
|
73
74
|
});
|
|
74
75
|
});
|
|
75
76
|
});
|
|
77
|
+
const memory = program
|
|
78
|
+
.command("memory")
|
|
79
|
+
.description("share Claude Code memory files (project/reference facts) for this repo");
|
|
80
|
+
memory
|
|
81
|
+
.command("push")
|
|
82
|
+
.description("publish this repo's shareable memory to the orphan branch")
|
|
83
|
+
.option("--all", "include personal user/feedback memories too", false)
|
|
84
|
+
.action(async (localOpts, cmd) => {
|
|
85
|
+
const g = globals(cmd);
|
|
86
|
+
await run(async () => {
|
|
87
|
+
const root = await repoRoot();
|
|
88
|
+
return memoryPush({ repoRoot: root, remote: g.remote, branch: g.branch, all: localOpts.all });
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
memory
|
|
92
|
+
.command("pull")
|
|
93
|
+
.description("fetch shared memory and update this repo's local MEMORY.md index")
|
|
94
|
+
.option("--all", "include personal user/feedback memories too", false)
|
|
95
|
+
.option("--force", "overwrite local copies even if they are newer", false)
|
|
96
|
+
.action(async (localOpts, cmd) => {
|
|
97
|
+
const g = globals(cmd);
|
|
98
|
+
await run(async () => {
|
|
99
|
+
const root = await repoRoot();
|
|
100
|
+
return memoryPull({
|
|
101
|
+
repoRoot: root,
|
|
102
|
+
remote: g.remote,
|
|
103
|
+
branch: g.branch,
|
|
104
|
+
all: localOpts.all,
|
|
105
|
+
force: localOpts.force,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
// `ccgs memory` with no subcommand: show its help on stdout and exit 0.
|
|
110
|
+
memory.action(() => {
|
|
111
|
+
memory.outputHelp();
|
|
112
|
+
process.exit(0);
|
|
113
|
+
});
|
|
76
114
|
// With no subcommand, print help to stdout and exit 0 — Commander's default is
|
|
77
115
|
// to print to stderr and exit 1, which looks like an error to anyone running
|
|
78
116
|
// the bare command (e.g. `npx claude-git-sessions`).
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MEMORY_DIR, MEMORY_INDEX } from "./constants.js";
|
|
4
|
+
import { projectDirForCwd } from "./paths.js";
|
|
5
|
+
const KNOWN_TYPES = ["user", "feedback", "project", "reference"];
|
|
6
|
+
/** Local memory directory for the repo's root slug. */
|
|
7
|
+
export function memoryDir(repoRoot) {
|
|
8
|
+
return path.join(projectDirForCwd(repoRoot), MEMORY_DIR);
|
|
9
|
+
}
|
|
10
|
+
function stripQuotes(s) {
|
|
11
|
+
const t = s.trim();
|
|
12
|
+
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
|
|
13
|
+
return t.slice(1, -1);
|
|
14
|
+
}
|
|
15
|
+
return t;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Minimal frontmatter reader for the known memory format. Deliberately not a
|
|
19
|
+
* full YAML parser — it only pulls out `name`, `description`, and the nested
|
|
20
|
+
* `type`, and degrades gracefully (type "unknown") on anything unexpected.
|
|
21
|
+
*/
|
|
22
|
+
export function parseFrontmatter(content) {
|
|
23
|
+
let name = null;
|
|
24
|
+
let description = null;
|
|
25
|
+
let type = "unknown";
|
|
26
|
+
if (content.startsWith("---")) {
|
|
27
|
+
// Find the closing delimiter line.
|
|
28
|
+
const rest = content.slice(3);
|
|
29
|
+
const endIdx = rest.search(/\n---\s*(\n|$)/);
|
|
30
|
+
if (endIdx !== -1) {
|
|
31
|
+
const block = rest.slice(0, endIdx);
|
|
32
|
+
for (const line of block.split("\n")) {
|
|
33
|
+
const n = line.match(/^\s*name:\s*(.+?)\s*$/);
|
|
34
|
+
if (n && name === null)
|
|
35
|
+
name = stripQuotes(n[1]);
|
|
36
|
+
const d = line.match(/^\s*description:\s*(.+?)\s*$/);
|
|
37
|
+
if (d && description === null)
|
|
38
|
+
description = stripQuotes(d[1]);
|
|
39
|
+
const t = line.match(/^\s*type:\s*([A-Za-z]+)/);
|
|
40
|
+
if (t) {
|
|
41
|
+
const v = t[1].toLowerCase();
|
|
42
|
+
if (KNOWN_TYPES.includes(v))
|
|
43
|
+
type = v;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { name, description, type };
|
|
49
|
+
}
|
|
50
|
+
/** Should a fact of `type` be shared? `all` overrides the type filter. */
|
|
51
|
+
export function isShareable(type, all) {
|
|
52
|
+
if (all)
|
|
53
|
+
return true;
|
|
54
|
+
return type === "project" || type === "reference";
|
|
55
|
+
}
|
|
56
|
+
/** kebab-or-filename -> a human "Title Case" string for the index link text. */
|
|
57
|
+
export function titleFor(file) {
|
|
58
|
+
const base = file.name ?? file.filename.replace(/\.md$/i, "");
|
|
59
|
+
return base
|
|
60
|
+
.replace(/[-_]+/g, " ")
|
|
61
|
+
.replace(/\s+/g, " ")
|
|
62
|
+
.trim()
|
|
63
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
64
|
+
}
|
|
65
|
+
/** Read all shareable-candidate memory files from the repo's memory dir. */
|
|
66
|
+
export function readLocalMemory(repoRoot) {
|
|
67
|
+
const dir = memoryDir(repoRoot);
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = fs.readdirSync(dir);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const filename of entries) {
|
|
77
|
+
if (!filename.endsWith(".md"))
|
|
78
|
+
continue;
|
|
79
|
+
if (filename === MEMORY_INDEX)
|
|
80
|
+
continue; // index handled separately
|
|
81
|
+
const p = path.join(dir, filename);
|
|
82
|
+
let content;
|
|
83
|
+
let mtime;
|
|
84
|
+
try {
|
|
85
|
+
const st = fs.statSync(p);
|
|
86
|
+
if (!st.isFile())
|
|
87
|
+
continue;
|
|
88
|
+
mtime = st.mtime;
|
|
89
|
+
content = fs.readFileSync(p, "utf8");
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const fm = parseFrontmatter(content);
|
|
95
|
+
out.push({
|
|
96
|
+
filename,
|
|
97
|
+
path: p,
|
|
98
|
+
content,
|
|
99
|
+
name: fm.name,
|
|
100
|
+
description: fm.description,
|
|
101
|
+
type: fm.type,
|
|
102
|
+
updatedAt: mtime.toISOString(),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
const BLOCK_START = "<!-- ccgs:shared-memory (managed by ccgs; do not edit by hand) -->";
|
|
108
|
+
const BLOCK_END = "<!-- /ccgs:shared-memory -->";
|
|
109
|
+
/** One index pointer line: `- [Title](file.md) — description`. */
|
|
110
|
+
export function indexLine(file) {
|
|
111
|
+
const tail = file.description ? ` — ${file.description}` : "";
|
|
112
|
+
return `- [${file.title}](${file.filename})${tail}`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Replace (or append) the ccgs-managed block in a MEMORY.md body with the given
|
|
116
|
+
* lines, leaving everything outside the markers untouched. Returns the new body.
|
|
117
|
+
*/
|
|
118
|
+
export function updateIndexBlock(existing, lines) {
|
|
119
|
+
const block = lines.length
|
|
120
|
+
? `${BLOCK_START}\n${lines.join("\n")}\n${BLOCK_END}`
|
|
121
|
+
: `${BLOCK_START}\n${BLOCK_END}`;
|
|
122
|
+
const startIdx = existing.indexOf(BLOCK_START);
|
|
123
|
+
const endIdx = existing.indexOf(BLOCK_END);
|
|
124
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
125
|
+
const before = existing.slice(0, startIdx);
|
|
126
|
+
const after = existing.slice(endIdx + BLOCK_END.length);
|
|
127
|
+
return before + block + after;
|
|
128
|
+
}
|
|
129
|
+
// Append, ensuring a blank line separates it from existing content.
|
|
130
|
+
const base = existing.trimEnd();
|
|
131
|
+
if (base === "")
|
|
132
|
+
return block + "\n";
|
|
133
|
+
return `${base}\n\n${block}\n`;
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-git-sessions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Share Claude Code sessions across a team through an orphan git branch in your existing repo.",
|
|
5
5
|
"keywords": ["claude", "claude-code", "git", "sessions", "cli"],
|
|
6
6
|
"license": "MIT",
|