pi-claude-store 0.1.1 → 0.1.3

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 CHANGED
@@ -68,12 +68,6 @@ by the directory itself.
68
68
  pi install npm:pi-claude-store
69
69
  ```
70
70
 
71
- Or track `main` (maintainers/development — updates with every merge):
72
-
73
- ```bash
74
- pi install git:github.com/yg-codes/pi-claude-store
75
- ```
76
-
77
71
  ## Development
78
72
 
79
73
  ```bash
@@ -60,7 +60,8 @@ export default function (pi: ExtensionAPI) {
60
60
  label: "Read memory",
61
61
  description:
62
62
  "Read one memory from the store shared with Claude Code. Takes the memory name — " +
63
- "the filename without the .md extension, as listed in the injected memory index.",
63
+ "the filename without the .md extension. memory_list shows the file stem in " +
64
+ "parentheses when it differs from the frontmatter name.",
64
65
  promptSnippet: "Read a stored memory by name",
65
66
  promptGuidelines: [
66
67
  "Call memory_read when an entry in the memory index looks relevant to the current task.",
@@ -73,7 +74,9 @@ export default function (pi: ExtensionAPI) {
73
74
  try {
74
75
  const memory = readMemory(dir, params.name);
75
76
  if (!memory) {
76
- const known = listMemories(dir).map((m) => m.name);
77
+ const known = listMemories(dir).map((m) =>
78
+ m.name && m.name !== m.file ? `${m.file ?? m.name} (name: ${m.name})` : (m.file ?? m.name),
79
+ );
77
80
  return {
78
81
  content: [
79
82
  {
@@ -307,7 +310,13 @@ export default function (pi: ExtensionAPI) {
307
310
  {
308
311
  type: "text" as const,
309
312
  text: memories.length
310
- ? memories.map((m) => `- ${m.name} — ${m.description}`).join("\n")
313
+ ? memories
314
+ .map((m) =>
315
+ m.name && m.name !== m.file
316
+ ? `- ${m.file ?? m.name} (name: ${m.name}) — ${m.description}`
317
+ : `- ${m.file ?? m.name} — ${m.description}`,
318
+ )
319
+ .join("\n")
311
320
  : "No memories stored for this project.",
312
321
  },
313
322
  ],
@@ -16,6 +16,19 @@
16
16
  *
17
17
  * Pure functions plus thin fs wrappers — no pi imports, so it is testable
18
18
  * with plain `node --test`.
19
+ *
20
+ * 2026-10-10 caveat fixes (each pinned by a "(caveat)" test):
21
+ * - Memories are addressable by file stem OR frontmatter `name:` — the real
22
+ * store has files where the two differ (underscore stems vs hyphen names,
23
+ * `feedback_`-prefixed stems, names with spaces), and read/delete by the
24
+ * advertised name used to fail while the error's "Available" list showed
25
+ * that very name.
26
+ * - Index pointer lines may carry a marker emoji before the bracket
27
+ * (`- 🔴 [T](a.md) — hook`); parse/render/upsert/remove keep the marker, so
28
+ * writes no longer leave stale duplicate lines and deletes no longer orphan
29
+ * them.
30
+ * - Updates preserve unknown frontmatter fields (originSessionId, …) and bump
31
+ * `metadata.modified` instead of flattening the block to a fixed shape.
19
32
  */
20
33
 
21
34
  import fs from "node:fs";
@@ -28,6 +41,8 @@ export interface IndexEntry {
28
41
  title: string;
29
42
  file: string;
30
43
  hook: string;
44
+ /** Marker before the bracket (`🔴` in `- 🔴 [T](a.md) — h`); absent on plain lines. */
45
+ marker?: string;
31
46
  }
32
47
 
33
48
  export interface Memory {
@@ -35,6 +50,8 @@ export interface Memory {
35
50
  description: string;
36
51
  type: string;
37
52
  body: string;
53
+ /** File stem (filename without .md) — the name memory_read resolves first. */
54
+ file?: string;
38
55
  }
39
56
 
40
57
  export interface WriteMemoryInput {
@@ -66,17 +83,29 @@ export function memoryDirFor(cwd: string): string {
66
83
  return path.join(os.homedir(), ".claude", "projects", sanitizePath(root), "memory");
67
84
  }
68
85
 
69
- /** Parse `- [Title](file.md) — hook` pointer lines, ignoring anything else. */
86
+ /**
87
+ * Parse `- [Title](file.md) — hook` pointer lines, ignoring anything else.
88
+ * A short marker of non-word symbols may sit between the dash and the
89
+ * bracket (`- 🔴 [T](a.md) — hook`); it is captured and preserved. Prose
90
+ * bullets whose prefix contains word characters (`- See the [x](y.md)`) are
91
+ * still ignored.
92
+ */
93
+ const INDEX_LINE_RE =
94
+ /^\s*-\s*([^\w\s()[\]]{1,4})?\s*\[([^\]]*)\]\(([^)]+)\)\s*(?:—\s*(.*))?$/;
95
+
70
96
  export function parseIndex(md: string): IndexEntry[] {
71
97
  const entries: IndexEntry[] = [];
72
98
  for (const line of md.split("\n")) {
73
- const match = /^\s*-\s*\[([^\]]*)\]\(([^)]+)\)\s*(?:—\s*(.*))?$/.exec(line);
99
+ const match = INDEX_LINE_RE.exec(line);
74
100
  if (!match) continue;
75
- entries.push({
76
- title: match[1].trim(),
77
- file: match[2].trim(),
78
- hook: (match[3] ?? "").trim(),
79
- });
101
+ const marker = match[1]?.trim();
102
+ const entry: IndexEntry = {
103
+ title: match[2].trim(),
104
+ file: match[3].trim(),
105
+ hook: (match[4] ?? "").trim(),
106
+ };
107
+ if (marker) entry.marker = marker;
108
+ entries.push(entry);
80
109
  }
81
110
  return entries;
82
111
  }
@@ -95,35 +124,132 @@ function sanitizeInline(value: string): string {
95
124
  function renderIndexLine(entry: IndexEntry): string {
96
125
  // Square brackets are stripped from the title: a `]` would end the link
97
126
  // text early and let title content capture the file field on re-parse,
98
- // forging an index entry that points at an attacker-chosen name. Brackets
99
- // in the hook are inert — everything after the em dash is captured as hook
100
- // text and cannot restructure the parse.
101
- const title = sanitizeInline(entry.title).replace(/[\[\]]/g, "");
127
+ // forging an index entry that points at an attacker-chosen name. The
128
+ // marker is stripped of brackets and parens for the same reason; hook
129
+ // content after the em dash cannot restructure the parse.
130
+ const marker = sanitizeInline(entry.marker ?? "").replace(/[[\]()]/g, "");
131
+ const title = sanitizeInline(entry.title).replace(/[[\]]/g, "");
102
132
  const hook = sanitizeInline(entry.hook);
103
133
  const file = sanitizeInline(entry.file);
104
- return hook ? `- [${title}](${file}) — ${hook}` : `- [${title}](${file})`;
134
+ const lead = marker ? `- ${marker} ` : "- ";
135
+ return hook ? `${lead}[${title}](${file}) — ${hook}` : `${lead}[${title}](${file})`;
105
136
  }
106
137
 
107
- /** Replace the pointer line for `entry.file` if present, otherwise append it. */
138
+ /**
139
+ * Replace the pointer line for `entry.file` if present, otherwise append it.
140
+ * The first matching line is rewritten (keeping its existing marker when the
141
+ * entry carries none); further duplicates for the same file are dropped —
142
+ * one pointer line per memory, per the store's own hygiene convention.
143
+ */
108
144
  export function upsertIndexLine(md: string, entry: IndexEntry): string {
109
- const rendered = renderIndexLine(entry);
110
145
  const lines = md.split("\n");
111
146
  while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
112
147
 
113
148
  let replaced = false;
114
- const next = lines.map((line) => {
149
+ const next: string[] = [];
150
+ for (const line of lines) {
115
151
  const parsed = parseIndex(line)[0];
116
152
  if (parsed && parsed.file === entry.file) {
117
- replaced = true;
118
- return rendered;
153
+ if (!replaced) {
154
+ next.push(renderIndexLine(entry.marker ? entry : { ...entry, marker: parsed.marker }));
155
+ replaced = true;
156
+ }
157
+ continue; // duplicate pointer line for the same file — collapse it
119
158
  }
120
- return line;
121
- });
159
+ next.push(line);
160
+ }
122
161
 
123
- if (!replaced) next.push(rendered);
162
+ if (!replaced) next.push(renderIndexLine(entry));
124
163
  return `${next.join("\n")}\n`;
125
164
  }
126
165
 
166
+ interface MemoryFile {
167
+ /** Filename (basename, with .md). */
168
+ fileName: string;
169
+ /** Filename without .md — the canonical address for reads. */
170
+ stem: string;
171
+ filePath: string;
172
+ memory: Memory;
173
+ }
174
+
175
+ /** All memory files in the directory (sorted, no symlinks, no MEMORY.md). */
176
+ function listMemoryFiles(dir: string): MemoryFile[] {
177
+ if (!fs.existsSync(dir)) return [];
178
+ return fs
179
+ .readdirSync(dir)
180
+ .filter((file) => file.endsWith(".md") && file !== INDEX_FILE)
181
+ .filter((file) => {
182
+ try {
183
+ return !fs.lstatSync(path.join(dir, file)).isSymbolicLink();
184
+ } catch {
185
+ return false;
186
+ }
187
+ })
188
+ .sort()
189
+ .map((file) => {
190
+ const filePath = path.join(dir, file);
191
+ const stem = path.basename(file, ".md");
192
+ const parsed = parseMemory(fs.readFileSync(filePath, "utf8"));
193
+ return {
194
+ fileName: file,
195
+ stem,
196
+ filePath,
197
+ memory: { ...parsed, name: parsed.name || stem, file: stem },
198
+ };
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Split a memory file into its raw frontmatter lines (null when absent or
204
+ * the closing delimiter sits beyond the 30-line window) and its body.
205
+ */
206
+ function splitFrontmatter(text: string): { frontmatter: string[] | null; body: string } {
207
+ const lines = text.split(/\r?\n/);
208
+ if (lines[0] !== "---") return { frontmatter: null, body: text.trim() };
209
+ const limit = Math.min(lines.length, FRONTMATTER_MAX_LINES);
210
+ for (let i = 1; i < limit; i++) {
211
+ if (lines[i] === "---") {
212
+ return {
213
+ frontmatter: lines.slice(1, i),
214
+ body: lines
215
+ .slice(i + 1)
216
+ .join("\n")
217
+ .replace(/^\s*\n/, "")
218
+ .trimEnd(),
219
+ };
220
+ }
221
+ }
222
+ return { frontmatter: null, body: text.trim() };
223
+ }
224
+
225
+ function setTopLevelKey(fm: string[], key: string, value: string): void {
226
+ const re = new RegExp(`^${key}:`);
227
+ const idx = fm.findIndex((line) => re.test(line));
228
+ if (idx >= 0) fm[idx] = `${key}: ${value}`;
229
+ else fm.push(`${key}: ${value}`);
230
+ }
231
+
232
+ /** Set `key` inside the indented block introduced by `block:`; insert the key when absent. */
233
+ function setNestedKey(fm: string[], block: string, key: string, value: string): void {
234
+ const blockRe = new RegExp(`^${block}:\\s*$`);
235
+ const blockIdx = fm.findIndex((line) => blockRe.test(line));
236
+ if (blockIdx === -1) {
237
+ fm.push(`${block}:`);
238
+ fm.push(` ${key}: ${value}`);
239
+ return;
240
+ }
241
+ let end = blockIdx + 1;
242
+ while (end < fm.length && /^\s+/.test(fm[end] ?? "")) end++;
243
+ const keyRe = new RegExp(`^\\s+${key}:`);
244
+ for (let i = blockIdx + 1; i < end; i++) {
245
+ if (keyRe.test(fm[i] ?? "")) {
246
+ fm[i] = ` ${key}: ${value}`;
247
+ return;
248
+ }
249
+ }
250
+ fm.splice(end, 0, ` ${key}: ${value}`);
251
+ }
252
+
127
253
  /**
128
254
  * Split a memory file into its frontmatter fields and body.
129
255
  *
@@ -132,38 +258,29 @@ export function upsertIndexLine(md: string, entry: IndexEntry): string {
132
258
  * Claude Code's rule. A body that happens to contain `---` further down is
133
259
  * left alone. The `type` field is read wherever it appears at the start of a
134
260
  * frontmatter line, so both the current `metadata.type` nesting and the older
135
- * top-level `type:` layout parse.
261
+ * top-level `type:` layout parse. Field values wrapped in one pair of matching
262
+ * quotes are unquoted — the store carries `description: "…"` files, and the
263
+ * raw value used to come back with its quotes baked in.
136
264
  */
137
265
  export function parseMemory(text: string): Memory {
138
- const lines = text.split(/\r?\n/);
139
- if (lines[0] !== "---") {
140
- return { name: "", description: "", type: "", body: text.trim() };
141
- }
142
-
143
- let close = -1;
144
- const limit = Math.min(lines.length, FRONTMATTER_MAX_LINES);
145
- for (let i = 1; i < limit; i++) {
146
- if (lines[i] === "---") {
147
- close = i;
148
- break;
149
- }
150
- }
151
- if (close === -1) {
152
- return { name: "", description: "", type: "", body: text.trim() };
153
- }
266
+ const { frontmatter, body } = splitFrontmatter(text);
267
+ if (!frontmatter) return { name: "", description: "", type: "", body: text.trim() };
154
268
 
155
- const frontmatter = lines.slice(1, close).join("\n");
269
+ const fm = frontmatter.join("\n");
156
270
  const field = (key: string): string => {
157
- const found = new RegExp(`^\\s*${key}:\\s*(.*)$`, "m").exec(frontmatter);
158
- return found ? found[1].trim() : "";
271
+ const found = new RegExp(`^\\s*${key}:\\s*(.*)$`, "m").exec(fm);
272
+ if (!found) return "";
273
+ let value = found[1].trim();
274
+ if (
275
+ value.length >= 2 &&
276
+ ((value.startsWith('"') && value.endsWith('"')) ||
277
+ (value.startsWith("'") && value.endsWith("'")))
278
+ ) {
279
+ value = value.slice(1, -1);
280
+ }
281
+ return value.trim();
159
282
  };
160
283
 
161
- const body = lines
162
- .slice(close + 1)
163
- .join("\n")
164
- .replace(/^\s*\n/, "")
165
- .trimEnd();
166
-
167
284
  return {
168
285
  name: field("name"),
169
286
  description: field("description"),
@@ -173,23 +290,30 @@ export function parseMemory(text: string): Memory {
173
290
  }
174
291
 
175
292
  /** Render a memory in the frontmatter shape Claude Code writes and reads. */
176
- export function serializeMemory(input: {
177
- name: string;
178
- description: string;
179
- type?: string;
180
- body?: string;
181
- }): string {
182
- return [
183
- "---",
184
- `name: ${input.name}`,
185
- `description: ${input.description}`,
186
- "metadata:",
187
- " node_type: memory",
188
- ` type: ${input.type || DEFAULT_TYPE}`,
189
- "---",
190
- "",
191
- `${(input.body ?? "").trim()}\n`,
192
- ].join("\n");
293
+ export function serializeMemory(
294
+ input: {
295
+ name: string;
296
+ description: string;
297
+ type?: string;
298
+ body?: string;
299
+ },
300
+ previousRaw?: string,
301
+ ): string {
302
+ // Start from the previous frontmatter when updating, so fields this
303
+ // extension does not model (originSessionId, custom metadata) survive.
304
+ const prev = previousRaw ? splitFrontmatter(previousRaw) : null;
305
+ const fm = prev?.frontmatter ? [...prev.frontmatter] : [];
306
+ const type = input.type || DEFAULT_TYPE;
307
+
308
+ setTopLevelKey(fm, "name", input.name);
309
+ setTopLevelKey(fm, "description", input.description);
310
+ // Older layout kept `type` top-level; keep such a line in step when present.
311
+ if (fm.some((line) => /^type:/.test(line))) setTopLevelKey(fm, "type", type);
312
+ setNestedKey(fm, "metadata", "node_type", "memory");
313
+ setNestedKey(fm, "metadata", "type", type);
314
+ setNestedKey(fm, "metadata", "modified", new Date().toISOString());
315
+
316
+ return ["---", ...fm, "---", "", `${(input.body ?? "").trim()}\n`].join("\n");
193
317
  }
194
318
 
195
319
  /** v1 is flat-only: one directory, single-segment names. */
@@ -201,6 +325,28 @@ function assertSafeName(name: string): void {
201
325
  }
202
326
  }
203
327
 
328
+ function isSafeName(name: string): boolean {
329
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && !name.includes("..");
330
+ }
331
+
332
+ /**
333
+ * Names that must keep throwing on sight — traversal, separators, absolute
334
+ * paths, hidden files, the reserved MEMORY index name. Everything else that
335
+ * fails the safe-name charset (spaces, punctuation, unicode) is merely an
336
+ * alias candidate: it can only match a frontmatter `name:`, never a path.
337
+ */
338
+ function isDangerousName(name: string): boolean {
339
+ return (
340
+ name === "" ||
341
+ name.includes("\0") ||
342
+ name.includes("..") ||
343
+ /[\\/]/.test(name) ||
344
+ /^[A-Za-z]:/.test(name) ||
345
+ name.startsWith(".") ||
346
+ name.toUpperCase() === "MEMORY"
347
+ );
348
+ }
349
+
204
350
  /**
205
351
  * Validate a memory name and resolve its file path inside `dir`, refusing
206
352
  * anything that escapes the directory — including through a symbolic link
@@ -211,24 +357,27 @@ function memoryFile(dir: string, name: string): string {
211
357
  return resolveMemoryFilePath(dir, name).filePath;
212
358
  }
213
359
 
360
+ /**
361
+ * Resolve an address to an existing memory file: by file stem first (the
362
+ * canonical address), then by frontmatter `name:` match. Security-rejected
363
+ * names throw exactly as the direct path always did; a merely non-charset
364
+ * name (spaces, hyphens-vs-underscores skew) falls through to the alias scan.
365
+ */
366
+ function findMemoryFile(dir: string, name: string): string | null {
367
+ if (isDangerousName(name)) memoryFile(dir, name); // throws
368
+ if (isSafeName(name)) {
369
+ const { filePath } = resolveMemoryFilePath(dir, name);
370
+ if (fs.existsSync(filePath)) return filePath;
371
+ }
372
+ for (const entry of listMemoryFiles(dir)) {
373
+ if (entry.memory.name === name) return entry.filePath;
374
+ }
375
+ return null;
376
+ }
377
+
214
378
  /** Every memory in the directory, sorted by name. Missing directory yields []. */
215
379
  export function listMemories(dir: string): Memory[] {
216
- if (!fs.existsSync(dir)) return [];
217
- return fs
218
- .readdirSync(dir)
219
- .filter((file) => file.endsWith(".md") && file !== INDEX_FILE)
220
- .filter((file) => {
221
- try {
222
- return !fs.lstatSync(path.join(dir, file)).isSymbolicLink();
223
- } catch {
224
- return false;
225
- }
226
- })
227
- .sort()
228
- .map((file) => {
229
- const parsed = parseMemory(fs.readFileSync(path.join(dir, file), "utf8"));
230
- return { ...parsed, name: parsed.name || path.basename(file, ".md") };
231
- });
380
+ return listMemoryFiles(dir).map((entry) => entry.memory);
232
381
  }
233
382
 
234
383
  /** Read the index file verbatim, or "" when it does not exist yet. */
@@ -238,10 +387,11 @@ export function readIndex(dir: string): string {
238
387
  }
239
388
 
240
389
  export function readMemory(dir: string, name: string): Memory | null {
241
- const file = memoryFile(dir, name);
242
- if (!fs.existsSync(file)) return null;
243
- const parsed = parseMemory(fs.readFileSync(file, "utf8"));
244
- return { ...parsed, name: parsed.name || name };
390
+ const filePath = findMemoryFile(dir, name);
391
+ if (!filePath) return null;
392
+ const stem = path.basename(filePath, ".md");
393
+ const parsed = parseMemory(fs.readFileSync(filePath, "utf8"));
394
+ return { ...parsed, name: parsed.name || stem, file: stem };
245
395
  }
246
396
 
247
397
  export interface WriteMemoryResult {
@@ -276,13 +426,19 @@ export function memoryDeletedSummary(name: string, existed: boolean): string {
276
426
  * empty. Returns true when a memory file existed.
277
427
  */
278
428
  export function deleteMemory(dir: string, name: string): boolean {
279
- const file = memoryFile(dir, name);
280
- const existed = fs.existsSync(file);
281
- if (existed) fs.rmSync(file);
282
-
429
+ const filePath = findMemoryFile(dir, name);
430
+ const existed = filePath !== null;
431
+ if (filePath) fs.rmSync(filePath);
432
+
433
+ // Clean both the resolved stem's line and (harmlessly) a line keyed by the
434
+ // literal name, so an orphaned pointer can never survive either spelling.
435
+ const stems = new Set<string>();
436
+ if (filePath) stems.add(path.basename(filePath, ".md"));
437
+ if (isSafeName(name)) stems.add(name);
283
438
  const indexFile = path.join(dir, INDEX_FILE);
284
- if (fs.existsSync(indexFile)) {
285
- const cleaned = removeIndexLine(fs.readFileSync(indexFile, "utf8"), `${name}.md`);
439
+ if (fs.existsSync(indexFile) && stems.size > 0) {
440
+ let cleaned = fs.readFileSync(indexFile, "utf8");
441
+ for (const stem of stems) cleaned = removeIndexLine(cleaned, `${stem}.md`);
286
442
  if (cleaned === "") fs.rmSync(indexFile);
287
443
  else fs.writeFileSync(indexFile, cleaned);
288
444
  }
@@ -291,10 +447,16 @@ export function deleteMemory(dir: string, name: string): boolean {
291
447
 
292
448
  /** Write (or overwrite) a memory and register it in the index. */
293
449
  export function writeMemory(dir: string, input: WriteMemoryInput): WriteMemoryResult {
294
- const file = memoryFile(dir, input.name);
450
+ assertSafeName(input.name);
295
451
  fs.mkdirSync(dir, { recursive: true });
296
452
 
297
- const previous = readMemory(dir, input.name);
453
+ // Resolve the target by stem OR frontmatter name so an update addressed
454
+ // by either spelling rewrites the same file instead of forking a second one.
455
+ const existing = findMemoryFile(dir, input.name);
456
+ const stem = existing ? path.basename(existing, ".md") : input.name;
457
+ const previousRaw = existing ? fs.readFileSync(existing, "utf8") : undefined;
458
+ const previous = existing && previousRaw !== undefined ? parseMemory(previousRaw) : null;
459
+ const filePath = existing ?? memoryFile(dir, input.name);
298
460
 
299
461
  const memory: Memory = {
300
462
  name: input.name,
@@ -302,15 +464,16 @@ export function writeMemory(dir: string, input: WriteMemoryInput): WriteMemoryRe
302
464
  // that forgets the summary should not silently blank it. Sanitized so a
303
465
  // description can never smuggle extra frontmatter keys into the file.
304
466
  description: sanitizeInline(input.description ?? "") || previous?.description || "",
305
- type: input.type || DEFAULT_TYPE,
467
+ type: input.type || previous?.type || DEFAULT_TYPE,
306
468
  body: input.body.trim(),
469
+ file: stem,
307
470
  };
308
471
 
309
- fs.writeFileSync(file, serializeMemory(memory));
472
+ fs.writeFileSync(filePath, serializeMemory(memory, previousRaw));
310
473
 
311
474
  const index = upsertIndexLine(readIndex(dir), {
312
475
  title: sanitizeInline(input.title ?? "") || input.name,
313
- file: `${input.name}.md`,
476
+ file: `${stem}.md`,
314
477
  hook: sanitizeInline(input.hook ?? "") || sanitizeInline(input.description ?? "") || "",
315
478
  });
316
479
  fs.writeFileSync(path.join(dir, INDEX_FILE), index);
@@ -333,7 +496,7 @@ export function buildMemoryPrompt(dir: string): string | null {
333
496
  : listMemories(dir).map((memory) =>
334
497
  renderIndexLine({
335
498
  title: memory.name,
336
- file: `${memory.name}.md`,
499
+ file: `${memory.file ?? memory.name}.md`,
337
500
  hook: memory.description,
338
501
  }),
339
502
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-claude-store",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Pi extension that reads and writes Claude Code's auto-memory store, keyed the way Claude Code keys it (canonical git root, Claude Code's sanitizePath)",
5
5
  "author": "yg-codes",
6
6
  "license": "MIT",