@ainote/mcp 1.3.1 → 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,5 +1,5 @@
1
- export { getSharedTools } from './shared-tools.js';
2
- export { getChatGptTools } from './chatgpt-tools.js';
3
- export { getDevDocTools } from './dev-doc-tools.js';
4
- export { getOnboardingTools } from './onboarding-tools.js';
5
- export { toSuccessContent, toErrorContent } from './internal/formatters.js';
1
+ export { getSharedTools } from './shared-tools.js';
2
+ export { getChatGptTools } from './chatgpt-tools.js';
3
+ export { getDevDocTools } from './dev-doc-tools.js';
4
+ export { getOnboardingTools } from './onboarding-tools.js';
5
+ export { toSuccessContent, toErrorContent } from './internal/formatters.js';
@@ -1,150 +1,150 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import os from 'os';
4
- import { canonicalizePath } from './path-mapper.js';
5
-
6
- const HOME = os.homedir();
7
- const CLAUDE_DIR = path.join(HOME, '.claude');
8
-
9
- const TEXT_EXTENSIONS = new Set([
10
- '.md', '.txt', '.json', '.yaml', '.yml', '.toml',
11
- '.js', '.ts', '.sh', '.py', '.rb', '.css', '.html'
12
- ]);
13
-
14
- function isTextFile(p) {
15
- return TEXT_EXTENSIONS.has(path.extname(p).toLowerCase());
16
- }
17
-
18
- function walkDir(root, maxFiles = 500) {
19
- const out = [];
20
- if (!fs.existsSync(root)) return out;
21
-
22
- function visit(dir) {
23
- if (out.length >= maxFiles) return;
24
- let entries;
25
- try {
26
- entries = fs.readdirSync(dir, { withFileTypes: true });
27
- } catch {
28
- return;
29
- }
30
- for (const e of entries) {
31
- if (e.name.startsWith('.')) continue;
32
- const full = path.join(dir, e.name);
33
- if (e.isDirectory()) {
34
- visit(full);
35
- } else if (e.isFile() && isTextFile(e.name)) {
36
- out.push(full);
37
- }
38
- if (out.length >= maxFiles) return;
39
- }
40
- }
41
- visit(root);
42
- return out;
43
- }
44
-
45
- function detectContentType(p) {
46
- const ext = path.extname(p).toLowerCase();
47
- if (ext === '.json') return 'json';
48
- if (ext === '.yaml' || ext === '.yml') return 'yaml';
49
- if (ext === '.md') return 'markdown';
50
- return 'text';
51
- }
52
-
53
- /**
54
- * Scan a Claude subdirectory (skills/agents/commands/hooks) and return file
55
- * descriptors ready for create_dev_doc calls.
56
- *
57
- * Returns: [{ title, content, content_type, local_path, relative_path }]
58
- * - title: unique per-file (e.g., "skills/ui-ux-pro-max/SKILL.md")
59
- * - local_path: canonicalized (~/...) so pull works cross-platform
60
- * - relative_path: path relative to ~/.claude/<subdir>/
61
- */
62
- export function scanClaudeSubdir(subdir, { maxFiles = 500 } = {}) {
63
- const root = path.join(CLAUDE_DIR, subdir);
64
- const files = walkDir(root, maxFiles);
65
- const results = [];
66
- for (const abs of files) {
67
- let content;
68
- try {
69
- content = fs.readFileSync(abs, 'utf8');
70
- } catch {
71
- continue;
72
- }
73
- const relativeFromClaude = path.relative(CLAUDE_DIR, abs);
74
- const relativeFromSubdir = path.relative(root, abs);
75
- results.push({
76
- title: relativeFromClaude.replace(/\\/g, '/'),
77
- content,
78
- content_type: detectContentType(abs),
79
- local_path: canonicalizePath(abs),
80
- relative_path: relativeFromSubdir.replace(/\\/g, '/'),
81
- size: content.length
82
- });
83
- }
84
- return results;
85
- }
86
-
87
- /**
88
- * Read mcpServers section from ~/.claude.json.
89
- * Returns the object (possibly empty) and the canonicalized source path.
90
- */
91
- export function readMcpServers() {
92
- const claudeJson = path.join(HOME, '.claude.json');
93
- if (!fs.existsSync(claudeJson)) {
94
- return { servers: {}, source: canonicalizePath(claudeJson), exists: false };
95
- }
96
- try {
97
- const raw = fs.readFileSync(claudeJson, 'utf8');
98
- const json = JSON.parse(raw);
99
- return {
100
- servers: json.mcpServers || {},
101
- source: canonicalizePath(claudeJson),
102
- exists: true
103
- };
104
- } catch (e) {
105
- throw new Error(`Failed to parse ~/.claude.json: ${e.message}`);
106
- }
107
- }
108
-
109
- /**
110
- * Sidecar pattern: write mcpServers snapshot to ~/.claude/mcp-servers.d/<name>.json
111
- * Never touches ~/.claude.json directly (avoids live-write corruption).
112
- */
113
- export function writeMcpServersSidecar(servers, { name = 'from-ainote' } = {}) {
114
- const sidecarDir = path.join(CLAUDE_DIR, 'mcp-servers.d');
115
- fs.mkdirSync(sidecarDir, { recursive: true });
116
- const target = path.join(sidecarDir, `${name}.json`);
117
- const tmp = `${target}.tmp.${process.pid}`;
118
- fs.writeFileSync(tmp, JSON.stringify({ mcpServers: servers }, null, 2));
119
- fs.renameSync(tmp, target);
120
- return canonicalizePath(target);
121
- }
122
-
123
- /**
124
- * Preview what would be synced. Read-only, fast.
125
- */
126
- export function discoverSyncTargets() {
127
- const result = {
128
- claude_dir: canonicalizePath(CLAUDE_DIR),
129
- targets: {}
130
- };
131
- for (const sub of ['skills', 'agents', 'commands', 'hooks']) {
132
- const files = scanClaudeSubdir(sub);
133
- result.targets[sub] = {
134
- file_count: files.length,
135
- total_bytes: files.reduce((a, b) => a + b.size, 0),
136
- samples: files.slice(0, 3).map(f => f.title)
137
- };
138
- }
139
- try {
140
- const mcp = readMcpServers();
141
- result.targets.mcp_servers = {
142
- file_count: Object.keys(mcp.servers).length,
143
- total_bytes: JSON.stringify(mcp.servers).length,
144
- samples: Object.keys(mcp.servers).slice(0, 5)
145
- };
146
- } catch (e) {
147
- result.targets.mcp_servers = { error: e.message };
148
- }
149
- return result;
150
- }
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { canonicalizePath } from './path-mapper.js';
5
+
6
+ const HOME = os.homedir();
7
+ const CLAUDE_DIR = path.join(HOME, '.claude');
8
+
9
+ const TEXT_EXTENSIONS = new Set([
10
+ '.md', '.txt', '.json', '.yaml', '.yml', '.toml',
11
+ '.js', '.ts', '.sh', '.py', '.rb', '.css', '.html'
12
+ ]);
13
+
14
+ function isTextFile(p) {
15
+ return TEXT_EXTENSIONS.has(path.extname(p).toLowerCase());
16
+ }
17
+
18
+ function walkDir(root, maxFiles = 500) {
19
+ const out = [];
20
+ if (!fs.existsSync(root)) return out;
21
+
22
+ function visit(dir) {
23
+ if (out.length >= maxFiles) return;
24
+ let entries;
25
+ try {
26
+ entries = fs.readdirSync(dir, { withFileTypes: true });
27
+ } catch {
28
+ return;
29
+ }
30
+ for (const e of entries) {
31
+ if (e.name.startsWith('.')) continue;
32
+ const full = path.join(dir, e.name);
33
+ if (e.isDirectory()) {
34
+ visit(full);
35
+ } else if (e.isFile() && isTextFile(e.name)) {
36
+ out.push(full);
37
+ }
38
+ if (out.length >= maxFiles) return;
39
+ }
40
+ }
41
+ visit(root);
42
+ return out;
43
+ }
44
+
45
+ function detectContentType(p) {
46
+ const ext = path.extname(p).toLowerCase();
47
+ if (ext === '.json') return 'json';
48
+ if (ext === '.yaml' || ext === '.yml') return 'yaml';
49
+ if (ext === '.md') return 'markdown';
50
+ return 'text';
51
+ }
52
+
53
+ /**
54
+ * Scan a Claude subdirectory (skills/agents/commands/hooks) and return file
55
+ * descriptors ready for create_dev_doc calls.
56
+ *
57
+ * Returns: [{ title, content, content_type, local_path, relative_path }]
58
+ * - title: unique per-file (e.g., "skills/ui-ux-pro-max/SKILL.md")
59
+ * - local_path: canonicalized (~/...) so pull works cross-platform
60
+ * - relative_path: path relative to ~/.claude/<subdir>/
61
+ */
62
+ export function scanClaudeSubdir(subdir, { maxFiles = 500 } = {}) {
63
+ const root = path.join(CLAUDE_DIR, subdir);
64
+ const files = walkDir(root, maxFiles);
65
+ const results = [];
66
+ for (const abs of files) {
67
+ let content;
68
+ try {
69
+ content = fs.readFileSync(abs, 'utf8');
70
+ } catch {
71
+ continue;
72
+ }
73
+ const relativeFromClaude = path.relative(CLAUDE_DIR, abs);
74
+ const relativeFromSubdir = path.relative(root, abs);
75
+ results.push({
76
+ title: relativeFromClaude.replace(/\\/g, '/'),
77
+ content,
78
+ content_type: detectContentType(abs),
79
+ local_path: canonicalizePath(abs),
80
+ relative_path: relativeFromSubdir.replace(/\\/g, '/'),
81
+ size: content.length
82
+ });
83
+ }
84
+ return results;
85
+ }
86
+
87
+ /**
88
+ * Read mcpServers section from ~/.claude.json.
89
+ * Returns the object (possibly empty) and the canonicalized source path.
90
+ */
91
+ export function readMcpServers() {
92
+ const claudeJson = path.join(HOME, '.claude.json');
93
+ if (!fs.existsSync(claudeJson)) {
94
+ return { servers: {}, source: canonicalizePath(claudeJson), exists: false };
95
+ }
96
+ try {
97
+ const raw = fs.readFileSync(claudeJson, 'utf8');
98
+ const json = JSON.parse(raw);
99
+ return {
100
+ servers: json.mcpServers || {},
101
+ source: canonicalizePath(claudeJson),
102
+ exists: true
103
+ };
104
+ } catch (e) {
105
+ throw new Error(`Failed to parse ~/.claude.json: ${e.message}`);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Sidecar pattern: write mcpServers snapshot to ~/.claude/mcp-servers.d/<name>.json
111
+ * Never touches ~/.claude.json directly (avoids live-write corruption).
112
+ */
113
+ export function writeMcpServersSidecar(servers, { name = 'from-ainote' } = {}) {
114
+ const sidecarDir = path.join(CLAUDE_DIR, 'mcp-servers.d');
115
+ fs.mkdirSync(sidecarDir, { recursive: true });
116
+ const target = path.join(sidecarDir, `${name}.json`);
117
+ const tmp = `${target}.tmp.${process.pid}`;
118
+ fs.writeFileSync(tmp, JSON.stringify({ mcpServers: servers }, null, 2));
119
+ fs.renameSync(tmp, target);
120
+ return canonicalizePath(target);
121
+ }
122
+
123
+ /**
124
+ * Preview what would be synced. Read-only, fast.
125
+ */
126
+ export function discoverSyncTargets() {
127
+ const result = {
128
+ claude_dir: canonicalizePath(CLAUDE_DIR),
129
+ targets: {}
130
+ };
131
+ for (const sub of ['skills', 'agents', 'commands', 'hooks']) {
132
+ const files = scanClaudeSubdir(sub);
133
+ result.targets[sub] = {
134
+ file_count: files.length,
135
+ total_bytes: files.reduce((a, b) => a + b.size, 0),
136
+ samples: files.slice(0, 3).map(f => f.title)
137
+ };
138
+ }
139
+ try {
140
+ const mcp = readMcpServers();
141
+ result.targets.mcp_servers = {
142
+ file_count: Object.keys(mcp.servers).length,
143
+ total_bytes: JSON.stringify(mcp.servers).length,
144
+ samples: Object.keys(mcp.servers).slice(0, 5)
145
+ };
146
+ } catch (e) {
147
+ result.targets.mcp_servers = { error: e.message };
148
+ }
149
+ return result;
150
+ }
@@ -1,88 +1,88 @@
1
- import { spawnSync } from 'child_process';
2
- import fs from 'fs';
3
- import os from 'os';
4
- import path from 'path';
5
- import { getAgeIdentity, getAgeRecipients } from './keychain.js';
6
-
7
- const TMPDIR = os.tmpdir();
8
-
9
- function ageBinary() {
10
- const r = spawnSync('age', ['--version'], { encoding: 'utf8' });
11
- if (r.status !== 0) {
12
- throw new Error('age binary not found. Install: brew install age');
13
- }
14
- return 'age';
15
- }
16
-
17
- function withTmpFile(prefix, body, fn) {
18
- const tmp = path.join(TMPDIR, `${prefix}.${process.pid}.${Date.now()}`);
19
- fs.writeFileSync(tmp, body, { mode: 0o600 });
20
- try {
21
- return fn(tmp);
22
- } finally {
23
- try { fs.unlinkSync(tmp); } catch {}
24
- }
25
- }
26
-
27
- /**
28
- * Encrypt plaintext for one or more recipients (age public keys).
29
- * Returns base64 ciphertext.
30
- */
31
- export async function encryptText(plaintext, { recipients } = {}) {
32
- const recps = recipients || await getAgeRecipients();
33
- if (!recps || recps.length === 0) {
34
- throw new Error('No age recipients available. Run sync_init_encryption first.');
35
- }
36
- ageBinary();
37
- return withTmpFile('ainote-enc-in', plaintext, (inputPath) => {
38
- const args = ['-a']; // armored output
39
- for (const r of recps) {
40
- args.push('-r', r);
41
- }
42
- args.push(inputPath);
43
- const r = spawnSync('age', args, { encoding: 'utf8' });
44
- if (r.status !== 0) {
45
- throw new Error(`age encrypt failed: ${r.stderr || 'unknown'}`);
46
- }
47
- return r.stdout;
48
- });
49
- }
50
-
51
- /**
52
- * Decrypt armored age ciphertext with this machine's identity from Keychain.
53
- */
54
- export async function decryptText(ciphertext) {
55
- ageBinary();
56
- const identity = await getAgeIdentity();
57
- if (!identity) {
58
- throw new Error('No age identity for this machine. Run sync_init_encryption first.');
59
- }
60
- return withTmpFile('ainote-id', identity, (idPath) =>
61
- withTmpFile('ainote-ct', ciphertext, (ctPath) => {
62
- const r = spawnSync('age', ['-d', '-i', idPath, ctPath], { encoding: 'utf8' });
63
- if (r.status !== 0) {
64
- throw new Error(`age decrypt failed: ${r.stderr || 'unknown'}`);
65
- }
66
- return r.stdout;
67
- })
68
- );
69
- }
70
-
71
- /**
72
- * Generate a new age keypair. Returns { identity, recipient }.
73
- * Identity is the private key (AGE-SECRET-KEY-1...), recipient is the public (age1...).
74
- */
75
- export function generateKeypair() {
76
- ageBinary();
77
- const r = spawnSync('age-keygen', [], { encoding: 'utf8' });
78
- if (r.status !== 0) {
79
- throw new Error(`age-keygen failed: ${r.stderr || 'unknown'}`);
80
- }
81
- const out = r.stdout;
82
- const recpMatch = out.match(/# public key: (age1[a-z0-9]+)/i);
83
- const idMatch = out.match(/(AGE-SECRET-KEY-1[A-Z0-9]+)/);
84
- if (!recpMatch || !idMatch) {
85
- throw new Error('Could not parse age-keygen output');
86
- }
87
- return { identity: out, recipient: recpMatch[1], identity_line: idMatch[1] };
88
- }
1
+ import { spawnSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { getAgeIdentity, getAgeRecipients } from './keychain.js';
6
+
7
+ const TMPDIR = os.tmpdir();
8
+
9
+ function ageBinary() {
10
+ const r = spawnSync('age', ['--version'], { encoding: 'utf8' });
11
+ if (r.status !== 0) {
12
+ throw new Error('age binary not found. Install: brew install age');
13
+ }
14
+ return 'age';
15
+ }
16
+
17
+ function withTmpFile(prefix, body, fn) {
18
+ const tmp = path.join(TMPDIR, `${prefix}.${process.pid}.${Date.now()}`);
19
+ fs.writeFileSync(tmp, body, { mode: 0o600 });
20
+ try {
21
+ return fn(tmp);
22
+ } finally {
23
+ try { fs.unlinkSync(tmp); } catch {}
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Encrypt plaintext for one or more recipients (age public keys).
29
+ * Returns base64 ciphertext.
30
+ */
31
+ export async function encryptText(plaintext, { recipients } = {}) {
32
+ const recps = recipients || await getAgeRecipients();
33
+ if (!recps || recps.length === 0) {
34
+ throw new Error('No age recipients available. Run sync_init_encryption first.');
35
+ }
36
+ ageBinary();
37
+ return withTmpFile('ainote-enc-in', plaintext, (inputPath) => {
38
+ const args = ['-a']; // armored output
39
+ for (const r of recps) {
40
+ args.push('-r', r);
41
+ }
42
+ args.push(inputPath);
43
+ const r = spawnSync('age', args, { encoding: 'utf8' });
44
+ if (r.status !== 0) {
45
+ throw new Error(`age encrypt failed: ${r.stderr || 'unknown'}`);
46
+ }
47
+ return r.stdout;
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Decrypt armored age ciphertext with this machine's identity from Keychain.
53
+ */
54
+ export async function decryptText(ciphertext) {
55
+ ageBinary();
56
+ const identity = await getAgeIdentity();
57
+ if (!identity) {
58
+ throw new Error('No age identity for this machine. Run sync_init_encryption first.');
59
+ }
60
+ return withTmpFile('ainote-id', identity, (idPath) =>
61
+ withTmpFile('ainote-ct', ciphertext, (ctPath) => {
62
+ const r = spawnSync('age', ['-d', '-i', idPath, ctPath], { encoding: 'utf8' });
63
+ if (r.status !== 0) {
64
+ throw new Error(`age decrypt failed: ${r.stderr || 'unknown'}`);
65
+ }
66
+ return r.stdout;
67
+ })
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Generate a new age keypair. Returns { identity, recipient }.
73
+ * Identity is the private key (AGE-SECRET-KEY-1...), recipient is the public (age1...).
74
+ */
75
+ export function generateKeypair() {
76
+ ageBinary();
77
+ const r = spawnSync('age-keygen', [], { encoding: 'utf8' });
78
+ if (r.status !== 0) {
79
+ throw new Error(`age-keygen failed: ${r.stderr || 'unknown'}`);
80
+ }
81
+ const out = r.stdout;
82
+ const recpMatch = out.match(/# public key: (age1[a-z0-9]+)/i);
83
+ const idMatch = out.match(/(AGE-SECRET-KEY-1[A-Z0-9]+)/);
84
+ if (!recpMatch || !idMatch) {
85
+ throw new Error('Could not parse age-keygen output');
86
+ }
87
+ return { identity: out, recipient: recpMatch[1], identity_line: idMatch[1] };
88
+ }
@@ -1,80 +1,80 @@
1
- export function toSuccessContent(payload, options = {}) {
2
- const { pretty = true } = options;
3
- const text = pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload);
4
-
5
- return {
6
- content: [
7
- {
8
- type: 'text',
9
- text
10
- }
11
- ]
12
- };
13
- }
14
-
15
- export function toErrorContent(error) {
16
- const message = error?.message ?? String(error);
17
- const details = error?.response?.data;
18
- const textParts = [`Error: ${message}`];
19
-
20
- if (details) {
21
- try {
22
- textParts.push(JSON.stringify(details, null, 2));
23
- } catch (stringifyError) {
24
- textParts.push(String(details));
25
- }
26
- }
27
-
28
- return {
29
- content: [
30
- {
31
- type: 'text',
32
- text: textParts.join('\n')
33
- }
34
- ],
35
- isError: true
36
- };
37
- }
38
-
39
- /**
40
- * Parse structured data from MCP response
41
- * Looks for JSON resource in content array and extracts structured data
42
- */
43
- export function parseStructuredData(result) {
44
- if (!result?.content || !Array.isArray(result.content)) {
45
- return null;
46
- }
47
-
48
- // Look for resource content with JSON data
49
- for (const item of result.content) {
50
- if (item.type === 'resource' && item.resource?.mimeType === 'application/json') {
51
- try {
52
- return JSON.parse(item.resource.text);
53
- } catch (error) {
54
- console.error('Failed to parse structured data:', error);
55
- return null;
56
- }
57
- }
58
- }
59
-
60
- return null;
61
- }
62
-
63
- /**
64
- * Enhance result with structured data annotation
65
- * Adds structured data to the response while preserving original text
66
- */
67
- export function enhanceWithStructuredData(result) {
68
- const structuredData = parseStructuredData(result);
69
-
70
- if (structuredData) {
71
- // Add metadata about structured data availability
72
- return {
73
- ...result,
74
- _structuredData: structuredData,
75
- _hasStructuredData: true
76
- };
77
- }
78
-
79
- return result;
80
- }
1
+ export function toSuccessContent(payload, options = {}) {
2
+ const { pretty = true } = options;
3
+ const text = pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload);
4
+
5
+ return {
6
+ content: [
7
+ {
8
+ type: 'text',
9
+ text
10
+ }
11
+ ]
12
+ };
13
+ }
14
+
15
+ export function toErrorContent(error) {
16
+ const message = error?.message ?? String(error);
17
+ const details = error?.response?.data;
18
+ const textParts = [`Error: ${message}`];
19
+
20
+ if (details) {
21
+ try {
22
+ textParts.push(JSON.stringify(details, null, 2));
23
+ } catch (stringifyError) {
24
+ textParts.push(String(details));
25
+ }
26
+ }
27
+
28
+ return {
29
+ content: [
30
+ {
31
+ type: 'text',
32
+ text: textParts.join('\n')
33
+ }
34
+ ],
35
+ isError: true
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Parse structured data from MCP response
41
+ * Looks for JSON resource in content array and extracts structured data
42
+ */
43
+ export function parseStructuredData(result) {
44
+ if (!result?.content || !Array.isArray(result.content)) {
45
+ return null;
46
+ }
47
+
48
+ // Look for resource content with JSON data
49
+ for (const item of result.content) {
50
+ if (item.type === 'resource' && item.resource?.mimeType === 'application/json') {
51
+ try {
52
+ return JSON.parse(item.resource.text);
53
+ } catch (error) {
54
+ console.error('Failed to parse structured data:', error);
55
+ return null;
56
+ }
57
+ }
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Enhance result with structured data annotation
65
+ * Adds structured data to the response while preserving original text
66
+ */
67
+ export function enhanceWithStructuredData(result) {
68
+ const structuredData = parseStructuredData(result);
69
+
70
+ if (structuredData) {
71
+ // Add metadata about structured data availability
72
+ return {
73
+ ...result,
74
+ _structuredData: structuredData,
75
+ _hasStructuredData: true
76
+ };
77
+ }
78
+
79
+ return result;
80
+ }