@reeledge/agent-tools 0.1.2 → 0.1.4

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,20 +1,75 @@
1
+ import fs from 'node:fs';
1
2
  import path from 'node:path';
2
- import { claudeMcpAddArgs } from '../mcp.js';
3
+ import { claudeMcpAddArgs, mcpEndpoint } from '../mcp.js';
3
4
  import { writeSkillTree } from '../skills.js';
4
5
  import { APP_LABELS, CONNECTOR_NAME, } from './types.js';
5
- /**
6
- * Claude Code adapter. Skills are per-skill `SKILL.md` directories under
7
- * `~/.claude/skills/` (overridable via `--skills-dir`); the MCP connector is
8
- * registered by spawning `claude mcp add --transport http …` (the documented
9
- * Claude Code path), which embeds the bearer token in an `Authorization` header.
10
- */
11
6
  /** Resolve the skills dir: explicit override, else `~/.claude/skills`. */
12
7
  function claudeSkillsDir(env) {
13
8
  return env.skillsDirOverride ?? path.join(env.homeDir, '.claude', 'skills');
14
9
  }
10
+ /**
11
+ * Absolute path to the Claude Desktop config file, or `null` on platforms where
12
+ * Claude Desktop doesn't ship (so the caller skips the desktop surface). Pure —
13
+ * `homeDir` + `platform` in, path out:
14
+ * - darwin → `<homeDir>/Library/Application Support/Claude/claude_desktop_config.json`
15
+ * - win32 → `<%APPDATA% or homeDir/AppData/Roaming>/Claude/claude_desktop_config.json`
16
+ * - else → null
17
+ */
18
+ export function claudeDesktopConfigPath(homeDir, platform) {
19
+ if (platform === 'darwin') {
20
+ return path.join(homeDir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
21
+ }
22
+ if (platform === 'win32') {
23
+ const appData = process.env.APPDATA ?? path.join(homeDir, 'AppData', 'Roaming');
24
+ return path.join(appData, 'Claude', 'claude_desktop_config.json');
25
+ }
26
+ return null;
27
+ }
28
+ /**
29
+ * The `mcp-remote` bridge entry for Claude Desktop. The header value lives in an
30
+ * `env` var and is referenced as `Authorization:${AUTH_HEADER}` (NO space):
31
+ * Claude Desktop splits each arg on spaces, so an inline `Bearer <token>` would
32
+ * be torn apart — keeping it in env is the supported escape hatch.
33
+ */
34
+ export function claudeDesktopEntry(endpoint, token) {
35
+ return {
36
+ command: 'npx',
37
+ args: ['-y', 'mcp-remote', endpoint, '--header', 'Authorization:${AUTH_HEADER}'],
38
+ env: { AUTH_HEADER: `Bearer ${token}` },
39
+ };
40
+ }
41
+ /**
42
+ * Merge a server entry into an existing `claude_desktop_config.json` body (or
43
+ * `undefined` when no file exists). Malformed JSON is replaced with a fresh file
44
+ * (mirroring how the CLI's own config loader tolerates corruption). Other servers
45
+ * + top-level keys are preserved. Returns pretty-printed JSON + trailing newline.
46
+ */
47
+ export function mergeClaudeDesktopConfig(existing, name, entry) {
48
+ let root = {};
49
+ if (existing && existing.trim() !== '') {
50
+ try {
51
+ const parsed = JSON.parse(existing);
52
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
53
+ root = parsed;
54
+ }
55
+ }
56
+ catch {
57
+ root = {};
58
+ }
59
+ }
60
+ const servers = root.mcpServers && typeof root.mcpServers === 'object' && !Array.isArray(root.mcpServers)
61
+ ? root.mcpServers
62
+ : {};
63
+ servers[name] = entry;
64
+ root.mcpServers = servers;
65
+ return `${JSON.stringify(root, null, 2)}\n`;
66
+ }
15
67
  /** Construct the Claude Code adapter for the given environment. */
16
68
  export function makeClaudeCodeAdapter(env) {
17
69
  const skillsDir = () => claudeSkillsDir(env);
70
+ const platform = () => env.platform ?? process.platform;
71
+ const log = (line) => env.log?.(line);
72
+ const desktopPath = () => claudeDesktopConfigPath(env.homeDir, platform());
18
73
  return {
19
74
  id: 'claude-code',
20
75
  label: APP_LABELS['claude-code'],
@@ -23,7 +78,30 @@ export function makeClaudeCodeAdapter(env) {
23
78
  return skills.flatMap((s) => writeSkillTree(skillsDir(), s.slug, s.files));
24
79
  },
25
80
  async configureMcp(serverUrl, token) {
26
- await env.exec('claude', claudeMcpAddArgs(CONNECTOR_NAME, serverUrl, token));
81
+ // 1. Claude Code CLI — best-effort and FULLY ISOLATED: a CLI failure must
82
+ // never block the desktop step below. A spawn ENOENT = not installed;
83
+ // "already exists" (a re-run / `update`, exit 1) or any other failure is
84
+ // logged as skipped, NOT thrown — the connector is already in place, and
85
+ // the desktop bridge still gets written.
86
+ try {
87
+ await env.exec('claude', claudeMcpAddArgs(CONNECTOR_NAME, serverUrl, token));
88
+ log(`Configured Claude Code CLI (${CONNECTOR_NAME})`);
89
+ }
90
+ catch (err) {
91
+ log(err.code === 'ENOENT'
92
+ ? 'Claude Code CLI not found — skipped'
93
+ : `Claude Code CLI not configured — skipped (${err.message})`);
94
+ }
95
+ // 2. Claude Desktop — macOS/Windows only; ALWAYS runs, independent of the CLI.
96
+ const cfgPath = desktopPath();
97
+ if (cfgPath !== null) {
98
+ const entry = claudeDesktopEntry(mcpEndpoint(serverUrl), token);
99
+ const existing = fs.existsSync(cfgPath) ? fs.readFileSync(cfgPath, 'utf8') : undefined;
100
+ const merged = mergeClaudeDesktopConfig(existing, CONNECTOR_NAME, entry);
101
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
102
+ fs.writeFileSync(cfgPath, merged, 'utf8');
103
+ log('Configured Claude Desktop (claude_desktop_config.json)');
104
+ }
27
105
  },
28
106
  dryRunPlan(serverUrl, token, skills) {
29
107
  const lines = [];
@@ -34,6 +112,14 @@ export function makeClaudeCodeAdapter(env) {
34
112
  }
35
113
  const args = claudeMcpAddArgs(CONNECTOR_NAME, serverUrl, token);
36
114
  lines.push(`would run: claude ${args.join(' ')}`);
115
+ const cfgPath = desktopPath();
116
+ if (cfgPath !== null) {
117
+ const entry = claudeDesktopEntry(mcpEndpoint(serverUrl), token);
118
+ lines.push(`would write ${cfgPath} with mcpServers.${CONNECTOR_NAME}:`);
119
+ for (const l of JSON.stringify(entry, null, 2).split('\n')) {
120
+ lines.push(` ${l}`);
121
+ }
122
+ }
37
123
  return lines;
38
124
  },
39
125
  };
package/dist/install.js CHANGED
@@ -98,6 +98,8 @@ async function run(opts, deps, mode, allowedSkills) {
98
98
  homeDir: deps.homeDir ?? os.homedir(),
99
99
  exec: deps.exec,
100
100
  skillsDirOverride: opts.skillsDir,
101
+ platform: process.platform,
102
+ log: deps.log,
101
103
  };
102
104
  const adapters = (deps.makeAdapters ?? defaultMakeAdapters)(appIds, env);
103
105
  // 1. Fetch the catalog. This is the first authenticated call — only once it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reeledge/agent-tools",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "Reel Edge agent-tools installer CLI — installs/updates the Reel Edge MCP connector + skills for Claude Code / Codex / Droid.",
6
6
  "type": "module",