@ainote/mcp 1.3.2 → 1.3.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.
@@ -1,98 +1,98 @@
1
- /**
2
- * Cross-platform secret storage for ainote sync (async).
3
- * - Primary: cross-keychain (macOS Keychain / libsecret / Windows Credential Manager)
4
- * - Fallback: ~/.config/ainote/age-identity.txt (0600)
5
- */
6
- import fs from 'fs';
7
- import path from 'path';
8
- import os from 'os';
9
- import { createRequire } from 'module';
10
-
11
- const require = createRequire(import.meta.url);
12
-
13
- const SERVICE = 'ainote-sync';
14
- const ACCOUNT_IDENTITY = 'age-identity';
15
- const ACCOUNT_RECIPIENTS = 'age-recipients';
16
-
17
- const FALLBACK_DIR = path.join(os.homedir(), '.config', 'ainote');
18
- const FALLBACK_IDENTITY = path.join(FALLBACK_DIR, 'age-identity.txt');
19
- const FALLBACK_RECIPIENTS = path.join(FALLBACK_DIR, 'age-recipients.json');
20
-
21
- let ck = null;
22
- function getKeychain() {
23
- if (ck !== null) return ck;
24
- try {
25
- ck = require('cross-keychain');
26
- } catch {
27
- ck = false;
28
- }
29
- return ck;
30
- }
31
-
32
- function fsRead(p) {
33
- try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
34
- }
35
-
36
- function fsWrite(p, value) {
37
- fs.mkdirSync(path.dirname(p), { recursive: true });
38
- fs.writeFileSync(p, value, { mode: 0o600 });
39
- }
40
-
41
- async function readSecret(account, fallbackPath) {
42
- const kc = getKeychain();
43
- if (kc && typeof kc.getPassword === 'function') {
44
- try {
45
- const v = await kc.getPassword(SERVICE, account);
46
- if (v) return v;
47
- } catch {}
48
- }
49
- return fsRead(fallbackPath);
50
- }
51
-
52
- async function writeSecret(account, value, fallbackPath) {
53
- const kc = getKeychain();
54
- if (kc && typeof kc.setPassword === 'function') {
55
- try {
56
- await kc.setPassword(SERVICE, account, value);
57
- return 'keychain';
58
- } catch {}
59
- }
60
- fsWrite(fallbackPath, value);
61
- return `file:${fallbackPath}`;
62
- }
63
-
64
- export async function getAgeIdentity() {
65
- return readSecret(ACCOUNT_IDENTITY, FALLBACK_IDENTITY);
66
- }
67
-
68
- export async function setAgeIdentity(identityText) {
69
- return writeSecret(ACCOUNT_IDENTITY, identityText, FALLBACK_IDENTITY);
70
- }
71
-
72
- export async function getAgeRecipients() {
73
- const raw = await readSecret(ACCOUNT_RECIPIENTS, FALLBACK_RECIPIENTS);
74
- if (!raw) return [];
75
- try {
76
- const v = JSON.parse(raw);
77
- return Array.isArray(v) ? v : [];
78
- } catch {
79
- return [];
80
- }
81
- }
82
-
83
- export async function setAgeRecipients(list) {
84
- return writeSecret(ACCOUNT_RECIPIENTS, JSON.stringify(list), FALLBACK_RECIPIENTS);
85
- }
86
-
87
- export async function addRecipient(recipient) {
88
- const current = await getAgeRecipients();
89
- if (!current.includes(recipient)) {
90
- current.push(recipient);
91
- await setAgeRecipients(current);
92
- }
93
- return current;
94
- }
95
-
96
- export function storageBackend() {
97
- return getKeychain() ? 'cross-keychain' : 'file-fallback';
98
- }
1
+ /**
2
+ * Cross-platform secret storage for ainote sync (async).
3
+ * - Primary: cross-keychain (macOS Keychain / libsecret / Windows Credential Manager)
4
+ * - Fallback: ~/.config/ainote/age-identity.txt (0600)
5
+ */
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import { createRequire } from 'module';
10
+
11
+ const require = createRequire(import.meta.url);
12
+
13
+ const SERVICE = 'ainote-sync';
14
+ const ACCOUNT_IDENTITY = 'age-identity';
15
+ const ACCOUNT_RECIPIENTS = 'age-recipients';
16
+
17
+ const FALLBACK_DIR = path.join(os.homedir(), '.config', 'ainote');
18
+ const FALLBACK_IDENTITY = path.join(FALLBACK_DIR, 'age-identity.txt');
19
+ const FALLBACK_RECIPIENTS = path.join(FALLBACK_DIR, 'age-recipients.json');
20
+
21
+ let ck = null;
22
+ function getKeychain() {
23
+ if (ck !== null) return ck;
24
+ try {
25
+ ck = require('cross-keychain');
26
+ } catch {
27
+ ck = false;
28
+ }
29
+ return ck;
30
+ }
31
+
32
+ function fsRead(p) {
33
+ try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
34
+ }
35
+
36
+ function fsWrite(p, value) {
37
+ fs.mkdirSync(path.dirname(p), { recursive: true });
38
+ fs.writeFileSync(p, value, { mode: 0o600 });
39
+ }
40
+
41
+ async function readSecret(account, fallbackPath) {
42
+ const kc = getKeychain();
43
+ if (kc && typeof kc.getPassword === 'function') {
44
+ try {
45
+ const v = await kc.getPassword(SERVICE, account);
46
+ if (v) return v;
47
+ } catch {}
48
+ }
49
+ return fsRead(fallbackPath);
50
+ }
51
+
52
+ async function writeSecret(account, value, fallbackPath) {
53
+ const kc = getKeychain();
54
+ if (kc && typeof kc.setPassword === 'function') {
55
+ try {
56
+ await kc.setPassword(SERVICE, account, value);
57
+ return 'keychain';
58
+ } catch {}
59
+ }
60
+ fsWrite(fallbackPath, value);
61
+ return `file:${fallbackPath}`;
62
+ }
63
+
64
+ export async function getAgeIdentity() {
65
+ return readSecret(ACCOUNT_IDENTITY, FALLBACK_IDENTITY);
66
+ }
67
+
68
+ export async function setAgeIdentity(identityText) {
69
+ return writeSecret(ACCOUNT_IDENTITY, identityText, FALLBACK_IDENTITY);
70
+ }
71
+
72
+ export async function getAgeRecipients() {
73
+ const raw = await readSecret(ACCOUNT_RECIPIENTS, FALLBACK_RECIPIENTS);
74
+ if (!raw) return [];
75
+ try {
76
+ const v = JSON.parse(raw);
77
+ return Array.isArray(v) ? v : [];
78
+ } catch {
79
+ return [];
80
+ }
81
+ }
82
+
83
+ export async function setAgeRecipients(list) {
84
+ return writeSecret(ACCOUNT_RECIPIENTS, JSON.stringify(list), FALLBACK_RECIPIENTS);
85
+ }
86
+
87
+ export async function addRecipient(recipient) {
88
+ const current = await getAgeRecipients();
89
+ if (!current.includes(recipient)) {
90
+ current.push(recipient);
91
+ await setAgeRecipients(current);
92
+ }
93
+ return current;
94
+ }
95
+
96
+ export function storageBackend() {
97
+ return getKeychain() ? 'cross-keychain' : 'file-fallback';
98
+ }
@@ -1,221 +1,221 @@
1
- import fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import { execSync } from 'child_process';
5
-
6
- /**
7
- * Cross-platform path mapper for ainote multi-device sync.
8
- *
9
- * Problem:
10
- * local_path is stored from the ORIGINAL machine (e.g. macOS).
11
- * When pulled on a DIFFERENT platform (e.g. WSL), paths don't match.
12
- *
13
- * Platform path patterns:
14
- * macOS: HOME=/Users/<user> project key: -Users-<user>
15
- * WSL: HOME=/home/<user> project key: -mnt-c-Users-<winuser> (CWD=/mnt/c/Users/<winuser>)
16
- * Linux: HOME=/home/<user> project key: -home-<user>
17
- * Windows Native: HOME=C:\Users\<user> project key: -C-Users-<user> or -Users-<user>
18
- *
19
- * Claude Code encodes CWD into project key by replacing / with -.
20
- * The "global" memory lives under the home/CWD project key.
21
- */
22
-
23
- // ── Platform detection ──
24
-
25
- function detectPlatform() {
26
- if (process.platform === 'darwin') return 'macos';
27
- if (process.platform === 'win32') return 'windows';
28
-
29
- // Linux — check for WSL
30
- try {
31
- const ver = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
32
- if (ver.includes('microsoft') || ver.includes('wsl')) return 'wsl';
33
- } catch { /* ignore */ }
34
-
35
- return 'linux';
36
- }
37
-
38
- const CURRENT_PLATFORM = detectPlatform();
39
- const HOME = os.homedir();
40
-
41
- // ── Windows home detection for WSL ──
42
-
43
- function detectWindowsHome() {
44
- if (CURRENT_PLATFORM !== 'wsl') return null;
45
-
46
- // Method 1: WSLHOME env var (user-set)
47
- if (process.env.WSLHOME) return process.env.WSLHOME;
48
-
49
- // Method 2: cmd.exe to get USERPROFILE
50
- try {
51
- const profile = execSync('cmd.exe /C "echo %USERPROFILE%"', { encoding: 'utf8', timeout: 3000 })
52
- .trim().replace(/\r?\n/g, '');
53
- if (profile && !profile.includes('%')) {
54
- // Convert C:\Users\Owner → /mnt/c/Users/Owner
55
- return profile.replace(/^([A-Z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, '/');
56
- }
57
- } catch { /* cmd.exe might not be available */ }
58
-
59
- // Method 3: scan /mnt/c/Users/
60
- try {
61
- const usersDir = '/mnt/c/Users';
62
- const skip = new Set(['Public', 'Default', 'Default User', 'All Users', 'desktop.ini']);
63
- const entries = fs.readdirSync(usersDir).filter(e => {
64
- if (skip.has(e)) return false;
65
- try { return fs.statSync(path.join(usersDir, e)).isDirectory(); } catch { return false; }
66
- });
67
- if (entries.length === 1) return path.join(usersDir, entries[0]);
68
- // Multiple users — try matching linux username
69
- const linuxUser = os.userInfo().username;
70
- const match = entries.find(e => e.toLowerCase() === linuxUser.toLowerCase());
71
- if (match) return path.join(usersDir, match);
72
- // Return first non-admin looking one
73
- if (entries.length > 0) return path.join(usersDir, entries[0]);
74
- } catch { /* not WSL or no /mnt/c */ }
75
-
76
- return null;
77
- }
78
-
79
- // Cache Windows home (lazy)
80
- let _winHome;
81
- let _winHomeResolved = false;
82
-
83
- function getWindowsHome() {
84
- if (!_winHomeResolved) {
85
- _winHome = detectWindowsHome();
86
- _winHomeResolved = true;
87
- }
88
- return _winHome;
89
- }
90
-
91
- // ── Claude project key ──
92
-
93
- /**
94
- * Get the current platform's "home" project key.
95
- * This is what Claude Code uses for the global memory directory.
96
- */
97
- function currentHomeProjectKey() {
98
- if (CURRENT_PLATFORM === 'wsl') {
99
- const winHome = getWindowsHome();
100
- if (winHome) return winHome.replace(/\//g, '-');
101
- }
102
- // macOS: -Users-seunghan, Linux: -home-seunghan
103
- return HOME.replace(/\//g, '-');
104
- }
105
-
106
- // ── Home-dir project key detection ──
107
- // These regex patterns match the "home directory" project keys from each platform.
108
-
109
- const HOME_KEY_PATTERNS = [
110
- /^-Users-[^-]+$/, // macOS: -Users-seunghan
111
- /^-mnt-[a-z]-Users-[^-]+$/, // WSL: -mnt-c-Users-Owner
112
- /^-home-[^-]+$/, // Linux: -home-seunghan
113
- /^-[A-Z]-Users-[^-]+$/, // Windows Native: -C-Users-Owner
114
- ];
115
-
116
- // Project key patterns (home key + project name suffix)
117
- const PROJECT_KEY_EXTRACTORS = [
118
- { pattern: /^-Users-[^-]+-(.+)$/, platform: 'macos' }, // -Users-seunghan-launchcrew
119
- { pattern: /^-mnt-[a-z]-Users-[^-]+-(.+)$/, platform: 'wsl' }, // -mnt-c-Users-Owner-launchcrew
120
- { pattern: /^-home-[^-]+-(.+)$/, platform: 'linux' }, // -home-seunghan-launchcrew
121
- { pattern: /^-[A-Z]-Users-[^-]+-(.+)$/, platform: 'windows' }, // -C-Users-Owner-launchcrew
122
- ];
123
-
124
- function isHomeProjectKey(key) {
125
- return HOME_KEY_PATTERNS.some(p => p.test(key));
126
- }
127
-
128
- function extractProjectName(key) {
129
- for (const { pattern } of PROJECT_KEY_EXTRACTORS) {
130
- const m = key.match(pattern);
131
- if (m) return m[1];
132
- }
133
- return null;
134
- }
135
-
136
- // ── Home directory prefix patterns ──
137
-
138
- const HOME_PREFIX_PATTERNS = [
139
- /^\/Users\/[^/]+/, // macOS
140
- /^\/mnt\/[a-z]\/Users\/[^/]+/, // WSL (Windows files)
141
- /^\/home\/[^/]+/, // Linux / WSL native
142
- /^[A-Z]:\\Users\\[^\\]+/, // Windows Native
143
- ];
144
-
145
- // ── Main path mapping ──
146
-
147
- /**
148
- * Transform a stored local_path to work on the current platform.
149
- *
150
- * Examples (stored on macOS, pulled on WSL):
151
- * ~/.claude/projects/-Users-seunghan/memory/MEMORY.md
152
- * → ~/.claude/projects/-mnt-c-Users-Owner/memory/MEMORY.md
153
- * → /home/seunghan/.claude/projects/-mnt-c-Users-Owner/memory/MEMORY.md
154
- *
155
- * /Users/seunghan/launchcrew/CLAUDE.md
156
- * → /home/seunghan/launchcrew/CLAUDE.md
157
- */
158
- export function mapPathForCurrentPlatform(storedPath) {
159
- if (!storedPath) return storedPath;
160
-
161
- let p = storedPath;
162
-
163
- // Step 1: Expand ~ to current HOME
164
- p = p.replace(/^~/, HOME);
165
-
166
- // Step 2: Replace any foreign home directory prefix with current HOME
167
- for (const pattern of HOME_PREFIX_PATTERNS) {
168
- if (pattern.test(p)) {
169
- p = p.replace(pattern, HOME);
170
- break;
171
- }
172
- }
173
-
174
- // Step 3: Map Claude project keys
175
- // Pattern: .../.claude/projects/<KEY>/...
176
- const claudeProjectRegex = /(\/\.claude\/projects\/)([-\w]+)(\/)/;
177
- const match = p.match(claudeProjectRegex);
178
- if (match) {
179
- const storedKey = match[2];
180
- const myHomeKey = currentHomeProjectKey();
181
-
182
- if (isHomeProjectKey(storedKey)) {
183
- // Home project key → replace with current platform's home key
184
- p = p.replace(claudeProjectRegex, `$1${myHomeKey}$3`);
185
- } else {
186
- // Project-specific key → extract project name + append to current home key
187
- const projectName = extractProjectName(storedKey);
188
- if (projectName) {
189
- const newKey = `${myHomeKey}-${projectName}`;
190
- p = p.replace(claudeProjectRegex, `$1${newKey}$3`);
191
- }
192
- }
193
- }
194
-
195
- return p;
196
- }
197
-
198
- /**
199
- * Normalize a local_path to canonical form before storing.
200
- * Replaces absolute home path with ~ for portability.
201
- */
202
- export function canonicalizePath(localPath) {
203
- if (!localPath) return localPath;
204
- return localPath.replace(new RegExp(`^${escapeRegex(HOME)}`), '~');
205
- }
206
-
207
- function escapeRegex(str) {
208
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
209
- }
210
-
211
- /**
212
- * Get current platform info (for diagnostics / tool output).
213
- */
214
- export function getPlatformInfo() {
215
- return {
216
- platform: CURRENT_PLATFORM,
217
- home: HOME,
218
- homeProjectKey: currentHomeProjectKey(),
219
- windowsHome: CURRENT_PLATFORM === 'wsl' ? getWindowsHome() : undefined,
220
- };
221
- }
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { execSync } from 'child_process';
5
+
6
+ /**
7
+ * Cross-platform path mapper for ainote multi-device sync.
8
+ *
9
+ * Problem:
10
+ * local_path is stored from the ORIGINAL machine (e.g. macOS).
11
+ * When pulled on a DIFFERENT platform (e.g. WSL), paths don't match.
12
+ *
13
+ * Platform path patterns:
14
+ * macOS: HOME=/Users/<user> project key: -Users-<user>
15
+ * WSL: HOME=/home/<user> project key: -mnt-c-Users-<winuser> (CWD=/mnt/c/Users/<winuser>)
16
+ * Linux: HOME=/home/<user> project key: -home-<user>
17
+ * Windows Native: HOME=C:\Users\<user> project key: -C-Users-<user> or -Users-<user>
18
+ *
19
+ * Claude Code encodes CWD into project key by replacing / with -.
20
+ * The "global" memory lives under the home/CWD project key.
21
+ */
22
+
23
+ // ── Platform detection ──
24
+
25
+ function detectPlatform() {
26
+ if (process.platform === 'darwin') return 'macos';
27
+ if (process.platform === 'win32') return 'windows';
28
+
29
+ // Linux — check for WSL
30
+ try {
31
+ const ver = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
32
+ if (ver.includes('microsoft') || ver.includes('wsl')) return 'wsl';
33
+ } catch { /* ignore */ }
34
+
35
+ return 'linux';
36
+ }
37
+
38
+ const CURRENT_PLATFORM = detectPlatform();
39
+ const HOME = os.homedir();
40
+
41
+ // ── Windows home detection for WSL ──
42
+
43
+ function detectWindowsHome() {
44
+ if (CURRENT_PLATFORM !== 'wsl') return null;
45
+
46
+ // Method 1: WSLHOME env var (user-set)
47
+ if (process.env.WSLHOME) return process.env.WSLHOME;
48
+
49
+ // Method 2: cmd.exe to get USERPROFILE
50
+ try {
51
+ const profile = execSync('cmd.exe /C "echo %USERPROFILE%"', { encoding: 'utf8', timeout: 3000 })
52
+ .trim().replace(/\r?\n/g, '');
53
+ if (profile && !profile.includes('%')) {
54
+ // Convert C:\Users\Owner → /mnt/c/Users/Owner
55
+ return profile.replace(/^([A-Z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, '/');
56
+ }
57
+ } catch { /* cmd.exe might not be available */ }
58
+
59
+ // Method 3: scan /mnt/c/Users/
60
+ try {
61
+ const usersDir = '/mnt/c/Users';
62
+ const skip = new Set(['Public', 'Default', 'Default User', 'All Users', 'desktop.ini']);
63
+ const entries = fs.readdirSync(usersDir).filter(e => {
64
+ if (skip.has(e)) return false;
65
+ try { return fs.statSync(path.join(usersDir, e)).isDirectory(); } catch { return false; }
66
+ });
67
+ if (entries.length === 1) return path.join(usersDir, entries[0]);
68
+ // Multiple users — try matching linux username
69
+ const linuxUser = os.userInfo().username;
70
+ const match = entries.find(e => e.toLowerCase() === linuxUser.toLowerCase());
71
+ if (match) return path.join(usersDir, match);
72
+ // Return first non-admin looking one
73
+ if (entries.length > 0) return path.join(usersDir, entries[0]);
74
+ } catch { /* not WSL or no /mnt/c */ }
75
+
76
+ return null;
77
+ }
78
+
79
+ // Cache Windows home (lazy)
80
+ let _winHome;
81
+ let _winHomeResolved = false;
82
+
83
+ function getWindowsHome() {
84
+ if (!_winHomeResolved) {
85
+ _winHome = detectWindowsHome();
86
+ _winHomeResolved = true;
87
+ }
88
+ return _winHome;
89
+ }
90
+
91
+ // ── Claude project key ──
92
+
93
+ /**
94
+ * Get the current platform's "home" project key.
95
+ * This is what Claude Code uses for the global memory directory.
96
+ */
97
+ function currentHomeProjectKey() {
98
+ if (CURRENT_PLATFORM === 'wsl') {
99
+ const winHome = getWindowsHome();
100
+ if (winHome) return winHome.replace(/\//g, '-');
101
+ }
102
+ // macOS: -Users-seunghan, Linux: -home-seunghan
103
+ return HOME.replace(/\//g, '-');
104
+ }
105
+
106
+ // ── Home-dir project key detection ──
107
+ // These regex patterns match the "home directory" project keys from each platform.
108
+
109
+ const HOME_KEY_PATTERNS = [
110
+ /^-Users-[^-]+$/, // macOS: -Users-seunghan
111
+ /^-mnt-[a-z]-Users-[^-]+$/, // WSL: -mnt-c-Users-Owner
112
+ /^-home-[^-]+$/, // Linux: -home-seunghan
113
+ /^-[A-Z]-Users-[^-]+$/, // Windows Native: -C-Users-Owner
114
+ ];
115
+
116
+ // Project key patterns (home key + project name suffix)
117
+ const PROJECT_KEY_EXTRACTORS = [
118
+ { pattern: /^-Users-[^-]+-(.+)$/, platform: 'macos' }, // -Users-seunghan-launchcrew
119
+ { pattern: /^-mnt-[a-z]-Users-[^-]+-(.+)$/, platform: 'wsl' }, // -mnt-c-Users-Owner-launchcrew
120
+ { pattern: /^-home-[^-]+-(.+)$/, platform: 'linux' }, // -home-seunghan-launchcrew
121
+ { pattern: /^-[A-Z]-Users-[^-]+-(.+)$/, platform: 'windows' }, // -C-Users-Owner-launchcrew
122
+ ];
123
+
124
+ function isHomeProjectKey(key) {
125
+ return HOME_KEY_PATTERNS.some(p => p.test(key));
126
+ }
127
+
128
+ function extractProjectName(key) {
129
+ for (const { pattern } of PROJECT_KEY_EXTRACTORS) {
130
+ const m = key.match(pattern);
131
+ if (m) return m[1];
132
+ }
133
+ return null;
134
+ }
135
+
136
+ // ── Home directory prefix patterns ──
137
+
138
+ const HOME_PREFIX_PATTERNS = [
139
+ /^\/Users\/[^/]+/, // macOS
140
+ /^\/mnt\/[a-z]\/Users\/[^/]+/, // WSL (Windows files)
141
+ /^\/home\/[^/]+/, // Linux / WSL native
142
+ /^[A-Z]:\\Users\\[^\\]+/, // Windows Native
143
+ ];
144
+
145
+ // ── Main path mapping ──
146
+
147
+ /**
148
+ * Transform a stored local_path to work on the current platform.
149
+ *
150
+ * Examples (stored on macOS, pulled on WSL):
151
+ * ~/.claude/projects/-Users-seunghan/memory/MEMORY.md
152
+ * → ~/.claude/projects/-mnt-c-Users-Owner/memory/MEMORY.md
153
+ * → /home/seunghan/.claude/projects/-mnt-c-Users-Owner/memory/MEMORY.md
154
+ *
155
+ * /Users/seunghan/launchcrew/CLAUDE.md
156
+ * → /home/seunghan/launchcrew/CLAUDE.md
157
+ */
158
+ export function mapPathForCurrentPlatform(storedPath) {
159
+ if (!storedPath) return storedPath;
160
+
161
+ let p = storedPath;
162
+
163
+ // Step 1: Expand ~ to current HOME
164
+ p = p.replace(/^~/, HOME);
165
+
166
+ // Step 2: Replace any foreign home directory prefix with current HOME
167
+ for (const pattern of HOME_PREFIX_PATTERNS) {
168
+ if (pattern.test(p)) {
169
+ p = p.replace(pattern, HOME);
170
+ break;
171
+ }
172
+ }
173
+
174
+ // Step 3: Map Claude project keys
175
+ // Pattern: .../.claude/projects/<KEY>/...
176
+ const claudeProjectRegex = /(\/\.claude\/projects\/)([-\w]+)(\/)/;
177
+ const match = p.match(claudeProjectRegex);
178
+ if (match) {
179
+ const storedKey = match[2];
180
+ const myHomeKey = currentHomeProjectKey();
181
+
182
+ if (isHomeProjectKey(storedKey)) {
183
+ // Home project key → replace with current platform's home key
184
+ p = p.replace(claudeProjectRegex, `$1${myHomeKey}$3`);
185
+ } else {
186
+ // Project-specific key → extract project name + append to current home key
187
+ const projectName = extractProjectName(storedKey);
188
+ if (projectName) {
189
+ const newKey = `${myHomeKey}-${projectName}`;
190
+ p = p.replace(claudeProjectRegex, `$1${newKey}$3`);
191
+ }
192
+ }
193
+ }
194
+
195
+ return p;
196
+ }
197
+
198
+ /**
199
+ * Normalize a local_path to canonical form before storing.
200
+ * Replaces absolute home path with ~ for portability.
201
+ */
202
+ export function canonicalizePath(localPath) {
203
+ if (!localPath) return localPath;
204
+ return localPath.replace(new RegExp(`^${escapeRegex(HOME)}`), '~');
205
+ }
206
+
207
+ function escapeRegex(str) {
208
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
209
+ }
210
+
211
+ /**
212
+ * Get current platform info (for diagnostics / tool output).
213
+ */
214
+ export function getPlatformInfo() {
215
+ return {
216
+ platform: CURRENT_PLATFORM,
217
+ home: HOME,
218
+ homeProjectKey: currentHomeProjectKey(),
219
+ windowsHome: CURRENT_PLATFORM === 'wsl' ? getWindowsHome() : undefined,
220
+ };
221
+ }