march-cli 0.1.33 → 0.1.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-cli",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "description": "March CLI — terminal-native coding agent with context reconstruction",
5
5
  "type": "module",
6
6
  "main": "./src/main.mjs",
@@ -0,0 +1,36 @@
1
+ export function isMemoryIdLike(value) {
2
+ return /^mem_[a-z0-9_-]+$/i.test(String(value ?? ""));
3
+ }
4
+
5
+ export function isSingleEditAway(left, right) {
6
+ if (left === right) return true;
7
+ if (Math.abs(left.length - right.length) > 1) return false;
8
+ if (left.length === right.length) return hasSingleSubstitution(left, right);
9
+ return hasSingleInsertionOrDeletion(left, right);
10
+ }
11
+
12
+ function hasSingleSubstitution(left, right) {
13
+ let mismatches = 0;
14
+ for (let i = 0; i < left.length; i += 1) {
15
+ if (left[i] !== right[i]) mismatches += 1;
16
+ if (mismatches > 1) return false;
17
+ }
18
+ return mismatches === 1;
19
+ }
20
+
21
+ function hasSingleInsertionOrDeletion(left, right) {
22
+ const shorter = left.length < right.length ? left : right;
23
+ const longer = left.length < right.length ? right : left;
24
+ let edits = 0;
25
+ for (let i = 0, j = 0; i < shorter.length && j < longer.length; ) {
26
+ if (shorter[i] === longer[j]) {
27
+ i += 1;
28
+ j += 1;
29
+ } else {
30
+ edits += 1;
31
+ j += 1;
32
+ if (edits > 1) return false;
33
+ }
34
+ }
35
+ return true;
36
+ }
@@ -13,6 +13,7 @@ import {
13
13
  import { scoreEntry, toHint } from "./markdown/markdown-recall.mjs";
14
14
  import { clearMarkdownMemoryIndex, loadMarkdownMemoryIndex, openMarkdownMemoryIndex, queryMarkdownMemoryIndex, replaceMarkdownMemoryIndex } from "./markdown/sqlite-index.mjs";
15
15
  import { softDeleteMemoryFile } from "./markdown/markdown-delete.mjs";
16
+ import { isMemoryIdLike, isSingleEditAway } from "./markdown/memory-id.mjs";
16
17
  import { openMarkdownRoot, searchMarkdownRoot } from "./search.mjs";
17
18
 
18
19
  export { formatRecallHints } from "./markdown/markdown-recall.mjs";
@@ -154,10 +155,9 @@ export class MarkdownMemoryStore {
154
155
  this.ensureFresh();
155
156
  const raw = String(identifier ?? "").trim();
156
157
  if (!raw) throw new Error("memory id or path is required");
157
- const entry = this.entries.get(raw);
158
- const path = entry ? entry.path : this.#resolveMemoryPath(raw);
159
- const opened = openMarkdownRoot({ root: this.root, path, ...options });
160
- return { ...opened, entry: entry ?? null };
158
+ const resolved = this.#resolveOpenTarget(raw);
159
+ const opened = openMarkdownRoot({ root: this.root, path: resolved.path, ...options });
160
+ return { ...opened, entry: resolved.entry, requestedId: resolved.requestedId };
161
161
  }
162
162
 
163
163
  save({ id = null, name = null, description = null, body = null, tags = null } = {}) {
@@ -261,11 +261,22 @@ export class MarkdownMemoryStore {
261
261
  return path;
262
262
  }
263
263
 
264
+ #resolveOpenTarget(raw) {
265
+ const exact = this.entries.get(raw);
266
+ if (exact) return { path: exact.path, entry: exact, requestedId: null };
267
+ if (!isMemoryIdLike(raw)) return { path: this.#resolveMemoryPath(raw), entry: null, requestedId: null };
268
+
269
+ const candidates = [...this.entries.values()].filter((entry) => isSingleEditAway(raw, entry.id));
270
+ if (candidates.length === 1) return { path: candidates[0].path, entry: candidates[0], requestedId: raw };
271
+ if (candidates.length > 1) {
272
+ throw new Error(`memory id is ambiguous: ${raw}; candidates: ${candidates.map((entry) => entry.id).join(", ")}`);
273
+ }
274
+ throw new Error(`memory not found: ${raw}`);
275
+ }
276
+
264
277
  #activeMemoryPaths() {
265
278
  return [...this.entries.values()]
266
279
  .filter((entry) => entry.status === "active")
267
280
  .map((entry) => entry.path);
268
281
  }
269
-
270
-
271
282
  }
@@ -146,12 +146,13 @@ function formatMemorySearchResults(results, requestedSource) {
146
146
 
147
147
  function formatLocalOpen(opened) {
148
148
  const range = opened.startLine && opened.endLine ? `lines: ${opened.startLine}-${opened.endLine}\n` : "";
149
- return `path: ${opened.path}\n${range}Use edit_file with this path for targeted edits.\n\n---\n${opened.content}`;
149
+ const correction = opened.requestedId && opened.entry?.id ? `matched id: ${opened.entry.id} (requested: ${opened.requestedId})\n` : "";
150
+ return `path: ${opened.path}\n${correction}${range}Use edit_file with this path for targeted edits.\n\ncontent:\n${opened.content}`;
150
151
  }
151
152
 
152
153
  function formatRemoteOpen(opened) {
153
154
  const range = opened.startLine && opened.endLine ? `lines: ${opened.startLine}-${opened.endLine}\n` : "";
154
- return `source: ${opened.source}\npath: ${opened.path}\n${range}Remote memory is read-only.\n\n---\n${opened.content}`;
155
+ return `source: ${opened.source}\npath: ${opened.path}\n${range}Remote memory is read-only.\n\ncontent:\n${opened.content}`;
155
156
  }
156
157
 
157
158
  function formatMemorySearchMiss(query, source) {