@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.
- package/LICENSE +20 -20
- package/README-ko.md +374 -374
- package/README.md +800 -800
- package/index.js +61 -61
- package/lib/auth/device-flow.js +285 -274
- package/lib/auth/mcp-key.js +40 -40
- package/lib/auth/oauth.js +18 -18
- package/lib/auth/refresh.js +78 -78
- package/lib/auth/strategy-factory.js +31 -31
- package/lib/auth/token-store.js +302 -286
- package/lib/cli/call.js +211 -211
- package/lib/cli/login.js +238 -238
- package/lib/cli/logout.js +39 -39
- package/lib/cli/signup.js +118 -118
- package/lib/cli/whoami.js +77 -77
- package/lib/core/api-client.js +1 -1
- package/lib/core/config-manager.js +106 -106
- package/lib/core/config.js +1 -1
- package/lib/core/mcp-server.js +2 -2
- package/lib/core/server-factory.js +82 -82
- package/lib/core/tool-registry.js +31 -31
- package/lib/tools/chatgpt-tools.js +93 -93
- package/lib/tools/dev-doc-tools.js +643 -643
- package/lib/tools/index.js +5 -5
- package/lib/tools/internal/claude-scanner.js +150 -150
- package/lib/tools/internal/encryption.js +88 -88
- package/lib/tools/internal/formatters.js +80 -80
- package/lib/tools/internal/keychain.js +98 -98
- package/lib/tools/internal/path-mapper.js +221 -221
- package/lib/tools/onboarding-tools.js +124 -124
- package/lib/tools/shared-tools.js +280 -280
- package/lib/utils/api-client.js +152 -152
- package/package.json +101 -101
- package/server.js +99 -99
|
@@ -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
|
+
}
|