memoir-cli 3.10.1 → 3.10.2
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 +1 -1
- package/src/adapters/index.js +17 -30
- package/src/session/state.js +51 -1
- package/src/utils/platform.js +47 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.2",
|
|
4
4
|
"mcpName": "io.github.camgitt/memoir",
|
|
5
5
|
"description": "Private, portable AI memory: synced across every coding tool and machine, end-to-end encrypted, free. One memory for Claude Code, Cursor, Copilot, Gemini + more — MCP-native, zero-knowledge, open source.",
|
|
6
6
|
"main": "src/index.js",
|
package/src/adapters/index.js
CHANGED
|
@@ -4,12 +4,21 @@ import path from 'path';
|
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { shouldIgnoreProject } from '../context/capture.js';
|
|
7
|
+
import { vscodeUserDir, vscodeGlobalStorage, xdgConfigDir } from '../utils/platform.js';
|
|
7
8
|
|
|
8
9
|
const home = os.homedir();
|
|
9
10
|
|
|
10
11
|
const isWin = process.platform === 'win32';
|
|
11
12
|
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
12
13
|
|
|
14
|
+
// VS Code-family config dirs — resolved per-OS (incl. Linux) via platform.js.
|
|
15
|
+
const cursorUserDir = vscodeUserDir('Cursor');
|
|
16
|
+
const windsurfUserDir = vscodeUserDir('Windsurf');
|
|
17
|
+
const clineStorageDir = vscodeGlobalStorage('saoudrizwan.claude-dev');
|
|
18
|
+
// Non-VSCode tools that store under AppData on Windows and XDG config on POSIX.
|
|
19
|
+
const copilotDir = isWin ? path.join(appData, 'GitHub Copilot') : path.join(xdgConfigDir(), 'github-copilot');
|
|
20
|
+
const zedDir = isWin ? path.join(appData, 'Zed') : path.join(xdgConfigDir(), 'zed');
|
|
21
|
+
|
|
13
22
|
export const adapters = [
|
|
14
23
|
{
|
|
15
24
|
name: 'Gemini CLI',
|
|
@@ -78,13 +87,9 @@ export const adapters = [
|
|
|
78
87
|
{
|
|
79
88
|
name: 'Cursor',
|
|
80
89
|
icon: '⚡',
|
|
81
|
-
source:
|
|
82
|
-
? path.join(appData, 'Cursor', 'User')
|
|
83
|
-
: path.join(home, 'Library', 'Application Support', 'Cursor', 'User'),
|
|
90
|
+
source: cursorUserDir,
|
|
84
91
|
filter: (src) => {
|
|
85
|
-
const cursorDir =
|
|
86
|
-
? path.join(appData, 'Cursor', 'User')
|
|
87
|
-
: path.join(home, 'Library', 'Application Support', 'Cursor', 'User');
|
|
92
|
+
const cursorDir = cursorUserDir;
|
|
88
93
|
const rel = path.relative(cursorDir, src);
|
|
89
94
|
if (src === cursorDir) return true;
|
|
90
95
|
const basename = path.basename(src);
|
|
@@ -100,13 +105,8 @@ export const adapters = [
|
|
|
100
105
|
{
|
|
101
106
|
name: 'GitHub Copilot',
|
|
102
107
|
icon: '🐙',
|
|
103
|
-
source:
|
|
104
|
-
? path.join(appData, 'GitHub Copilot')
|
|
105
|
-
: path.join(home, '.config', 'github-copilot'),
|
|
108
|
+
source: copilotDir,
|
|
106
109
|
filter: (src) => {
|
|
107
|
-
const copilotDir = isWin
|
|
108
|
-
? path.join(appData, 'GitHub Copilot')
|
|
109
|
-
: path.join(home, '.config', 'github-copilot');
|
|
110
110
|
if (src === copilotDir) return true;
|
|
111
111
|
const basename = path.basename(src);
|
|
112
112
|
// Only sync config — skip auth tokens and version files
|
|
@@ -117,13 +117,9 @@ export const adapters = [
|
|
|
117
117
|
{
|
|
118
118
|
name: 'Windsurf',
|
|
119
119
|
icon: '🏄',
|
|
120
|
-
source:
|
|
121
|
-
? path.join(appData, 'Windsurf', 'User')
|
|
122
|
-
: path.join(home, 'Library', 'Application Support', 'Windsurf', 'User'),
|
|
120
|
+
source: windsurfUserDir,
|
|
123
121
|
filter: (src) => {
|
|
124
|
-
const windsurfDir =
|
|
125
|
-
? path.join(appData, 'Windsurf', 'User')
|
|
126
|
-
: path.join(home, 'Library', 'Application Support', 'Windsurf', 'User');
|
|
122
|
+
const windsurfDir = windsurfUserDir;
|
|
127
123
|
const rel = path.relative(windsurfDir, src);
|
|
128
124
|
if (src === windsurfDir) return true;
|
|
129
125
|
const basename = path.basename(src);
|
|
@@ -138,13 +134,8 @@ export const adapters = [
|
|
|
138
134
|
{
|
|
139
135
|
name: 'Zed',
|
|
140
136
|
icon: '🔶',
|
|
141
|
-
source:
|
|
142
|
-
? path.join(appData, 'Zed')
|
|
143
|
-
: path.join(home, '.config', 'zed'),
|
|
137
|
+
source: zedDir,
|
|
144
138
|
filter: (src) => {
|
|
145
|
-
const zedDir = isWin
|
|
146
|
-
? path.join(appData, 'Zed')
|
|
147
|
-
: path.join(home, '.config', 'zed');
|
|
148
139
|
const rel = path.relative(zedDir, src);
|
|
149
140
|
if (src === zedDir) return true;
|
|
150
141
|
const basename = path.basename(src);
|
|
@@ -163,13 +154,9 @@ export const adapters = [
|
|
|
163
154
|
{
|
|
164
155
|
name: 'Cline',
|
|
165
156
|
icon: '🤖',
|
|
166
|
-
source:
|
|
167
|
-
? path.join(appData, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')
|
|
168
|
-
: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev'),
|
|
157
|
+
source: clineStorageDir,
|
|
169
158
|
filter: (src) => {
|
|
170
|
-
const clineDir =
|
|
171
|
-
? path.join(appData, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')
|
|
172
|
-
: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev');
|
|
159
|
+
const clineDir = clineStorageDir;
|
|
173
160
|
const rel = path.relative(clineDir, src);
|
|
174
161
|
if (src === clineDir) return true;
|
|
175
162
|
const basename = path.basename(src);
|
package/src/session/state.js
CHANGED
|
@@ -33,6 +33,9 @@ export { SCHEMA_VERSION, emptySession };
|
|
|
33
33
|
// Prevents unbounded growth of the live pinned block.
|
|
34
34
|
const MAX_GOALS = 3;
|
|
35
35
|
const MAX_NEXT = 8;
|
|
36
|
+
// Completion tombstones kept so merges can't resurrect finished actions.
|
|
37
|
+
// Must outlive every stale copy that might still carry the item.
|
|
38
|
+
const MAX_COMPLETED_TOMBSTONES = 50;
|
|
36
39
|
const MAX_QUESTIONS = 5;
|
|
37
40
|
const MAX_DECISIONS_RECENT = 10;
|
|
38
41
|
const MAX_HISTORY = 30;
|
|
@@ -225,7 +228,20 @@ export async function completeNext(textOrIndex) {
|
|
|
225
228
|
}
|
|
226
229
|
const completed = idx >= 0;
|
|
227
230
|
if (completed) {
|
|
228
|
-
|
|
231
|
+
// Removal alone is not enough: any merge with a copy that still holds
|
|
232
|
+
// this item (the push-side backup, another machine, a stale MCP-process
|
|
233
|
+
// write) re-unions it straight back — the "completed actions resurrect"
|
|
234
|
+
// bug. So completion also records a tombstone that merges consult.
|
|
235
|
+
// Temporal, not absolute like decisions' `hidden`: a re-add whose
|
|
236
|
+
// `added` postdates `done_at` is a deliberate revival and survives.
|
|
237
|
+
const [removed] = state.current.next_actions.splice(idx, 1);
|
|
238
|
+
const key = removed.text.trim().toLowerCase();
|
|
239
|
+
state.current.completed_actions = [
|
|
240
|
+
{ text: removed.text, done_at: new Date().toISOString() },
|
|
241
|
+
...(state.current.completed_actions || []).filter(
|
|
242
|
+
c => c && c.text && c.text.trim().toLowerCase() !== key
|
|
243
|
+
),
|
|
244
|
+
].slice(0, MAX_COMPLETED_TOMBSTONES);
|
|
229
245
|
}
|
|
230
246
|
await writeSession(state);
|
|
231
247
|
// Only when something was actually completed — the event should mean
|
|
@@ -312,6 +328,25 @@ export function mergeSessions(local, remote) {
|
|
|
312
328
|
history: mergeHistory(local.history, remote.history),
|
|
313
329
|
};
|
|
314
330
|
|
|
331
|
+
// Completed-action tombstones beat the union above. unionByText can only
|
|
332
|
+
// union; it cannot represent "this used to exist and was finished," so a
|
|
333
|
+
// completed item surviving in ANY stale copy resurrected on every merge —
|
|
334
|
+
// the intermittent completeNext no-op observed in production (2026-08-03).
|
|
335
|
+
// Temporal on purpose: an item re-ADDED after its done_at is a deliberate
|
|
336
|
+
// revival and must survive, so tombstones only suppress copies whose
|
|
337
|
+
// `added` predates the completion.
|
|
338
|
+
const tombstones = unionTombstones(
|
|
339
|
+
local.current?.completed_actions,
|
|
340
|
+
remote.current?.completed_actions
|
|
341
|
+
);
|
|
342
|
+
merged.current.completed_actions = tombstones;
|
|
343
|
+
merged.current.next_actions = merged.current.next_actions.filter(a => {
|
|
344
|
+
const t = tombstones.find(
|
|
345
|
+
c => c.text.trim().toLowerCase() === a.text.trim().toLowerCase()
|
|
346
|
+
);
|
|
347
|
+
return !t || new Date(a.added || 0) > new Date(t.done_at);
|
|
348
|
+
});
|
|
349
|
+
|
|
315
350
|
// machines: union last_seen per id (take the newer)
|
|
316
351
|
for (const [id, entry] of Object.entries(remote.machines || {})) {
|
|
317
352
|
const existing = merged.machines[id];
|
|
@@ -359,6 +394,21 @@ function unionByText(a = [], b = [], dateField, cap) {
|
|
|
359
394
|
.slice(0, cap);
|
|
360
395
|
}
|
|
361
396
|
|
|
397
|
+
function unionTombstones(a = [], b = []) {
|
|
398
|
+
const byText = new Map();
|
|
399
|
+
for (const item of [...(a || []), ...(b || [])]) {
|
|
400
|
+
if (!item || !item.text || !item.done_at) continue;
|
|
401
|
+
const key = item.text.trim().toLowerCase();
|
|
402
|
+
const existing = byText.get(key);
|
|
403
|
+
if (!existing || new Date(item.done_at) > new Date(existing.done_at)) {
|
|
404
|
+
byText.set(key, item);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return Array.from(byText.values())
|
|
408
|
+
.sort((x, y) => new Date(y.done_at) - new Date(x.done_at))
|
|
409
|
+
.slice(0, MAX_COMPLETED_TOMBSTONES);
|
|
410
|
+
}
|
|
411
|
+
|
|
362
412
|
function mergeHistory(a = [], b = []) {
|
|
363
413
|
const seen = new Set();
|
|
364
414
|
const all = [...a, ...b].filter(h => h && h.date);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Single source of truth for OS-specific config locations.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: before this module, every adapter branched on a single
|
|
4
|
+
// `isWin` flag — `isWin ? <windows> : <macOS>`. On Linux, `process.platform`
|
|
5
|
+
// is `'linux'`, so those ternaries silently fell through to the *macOS* path
|
|
6
|
+
// (`~/Library/Application Support/...`), which doesn't exist on Linux. memoir
|
|
7
|
+
// would then detect zero tools, sync nothing, and the user would churn without
|
|
8
|
+
// any error — the "silent-zero-memory activation cliff."
|
|
9
|
+
//
|
|
10
|
+
// Every function is parameterized by { platform, env, home } so the three OS
|
|
11
|
+
// branches can be unit-tested from any machine, not just the target OS. Runtime
|
|
12
|
+
// callers omit the options and get the live platform.
|
|
13
|
+
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
|
|
17
|
+
const HOME = os.homedir();
|
|
18
|
+
|
|
19
|
+
// VS Code-family per-user config base: <root>/<App>/User
|
|
20
|
+
// win32 : %APPDATA%/<App>/User
|
|
21
|
+
// darwin : ~/Library/Application Support/<App>/User
|
|
22
|
+
// linux : $XDG_CONFIG_HOME (or ~/.config)/<App>/User
|
|
23
|
+
export function vscodeUserDir(appName, { platform = process.platform, env = process.env, home = HOME } = {}) {
|
|
24
|
+
if (platform === 'win32') {
|
|
25
|
+
const appData = env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
26
|
+
return path.join(appData, appName, 'User');
|
|
27
|
+
}
|
|
28
|
+
if (platform === 'darwin') {
|
|
29
|
+
return path.join(home, 'Library', 'Application Support', appName, 'User');
|
|
30
|
+
}
|
|
31
|
+
// linux + anything else POSIX-y
|
|
32
|
+
const xdg = env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
33
|
+
return path.join(xdg, appName, 'User');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A VS Code extension's globalStorage dir (e.g. Cline lives under the base
|
|
37
|
+
// "Code" install, not its own app dir).
|
|
38
|
+
export function vscodeGlobalStorage(extId, opts = {}) {
|
|
39
|
+
return path.join(vscodeUserDir('Code', opts), 'globalStorage', extId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// XDG-aware ~/.config base, for non-VSCode tools that already store there
|
|
43
|
+
// (zed, github-copilot). Exposed so callers don't re-hardcode ~/.config and
|
|
44
|
+
// drift from XDG_CONFIG_HOME.
|
|
45
|
+
export function xdgConfigDir({ env = process.env, home = HOME } = {}) {
|
|
46
|
+
return env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
47
|
+
}
|