@reeledge/agent-tools 0.1.4 → 0.1.6

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.
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { claudeMcpAddArgs, mcpEndpoint } from '../mcp.js';
4
4
  import { writeSkillTree } from '../skills.js';
5
+ import { buildLocalPlugin, claudePluginDir, pluginDryRunLines, registerAndInstallPlugin, } from './claude-plugin.js';
5
6
  import { APP_LABELS, CONNECTOR_NAME, } from './types.js';
6
7
  /** Resolve the skills dir: explicit override, else `~/.claude/skills`. */
7
8
  function claudeSkillsDir(env) {
@@ -70,29 +71,63 @@ export function makeClaudeCodeAdapter(env) {
70
71
  const platform = () => env.platform ?? process.platform;
71
72
  const log = (line) => env.log?.(line);
72
73
  const desktopPath = () => claudeDesktopConfigPath(env.homeDir, platform());
74
+ const pluginDir = () => claudePluginDir();
75
+ // Captured by `writeSkills` so `configureMcp` can fall back to a loose
76
+ // `~/.claude/skills/` write when the `claude` binary is absent (and the plugin
77
+ // therefore can't be installed). In the real flow `writeSkills` always runs
78
+ // first (install.ts); left `[]` the fallback simply writes nothing.
79
+ let pending = [];
73
80
  return {
74
81
  id: 'claude-code',
75
82
  label: APP_LABELS['claude-code'],
76
83
  skillsDir,
77
84
  writeSkills(skills) {
78
- return skills.flatMap((s) => writeSkillTree(skillsDir(), s.slug, s.files));
85
+ pending = skills;
86
+ // Build the self-contained local plugin — the primary skills path. The
87
+ // actual register/install (and any loose-skills fallback) happens in
88
+ // `configureMcp`, which can await the `claude` CLI.
89
+ return buildLocalPlugin(pluginDir(), skills);
79
90
  },
80
91
  async configureMcp(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.
92
+ // 1. Claude Code CLI MCP connector — best-effort and FULLY ISOLATED: a CLI
93
+ // failure must never block the plugin/desktop steps below. A spawn ENOENT
94
+ // = not installed; "already exists" (a re-run / `update`, exit 1) or any
95
+ // other failure is logged as skipped, NOT thrown.
96
+ let claudeMissing = false;
86
97
  try {
87
98
  await env.exec('claude', claudeMcpAddArgs(CONNECTOR_NAME, serverUrl, token));
88
99
  log(`Configured Claude Code CLI (${CONNECTOR_NAME})`);
89
100
  }
90
101
  catch (err) {
91
- log(err.code === 'ENOENT'
92
- ? 'Claude Code CLI not found — skipped'
93
- : `Claude Code CLI not configured — skipped (${err.message})`);
102
+ if (err.code === 'ENOENT') {
103
+ claudeMissing = true;
104
+ log('Claude Code CLI not found — skipped');
105
+ }
106
+ else {
107
+ log(`Claude Code CLI not configured — skipped (${err.message})`);
108
+ }
109
+ }
110
+ // 2. Skills plugin — register the local marketplace + install the plugin so
111
+ // it auto-loads in the Claude Code CLI AND the Desktop app. If `claude`
112
+ // is absent, fall back to a loose `~/.claude/skills/` write so the CLI
113
+ // still gets the skills.
114
+ if (!claudeMissing) {
115
+ const { claudeFound } = await registerAndInstallPlugin(env.exec, pluginDir());
116
+ if (claudeFound) {
117
+ log('Installed skills plugin (auto-loads in Claude Code + Desktop)');
118
+ }
119
+ else {
120
+ claudeMissing = true;
121
+ }
122
+ }
123
+ if (claudeMissing && pending.length > 0) {
124
+ const written = pending.flatMap((s) => writeSkillTree(skillsDir(), s.slug, s.files));
125
+ for (const file of written)
126
+ log(` wrote ${file}`);
127
+ log('Claude CLI unavailable — wrote skills to ~/.claude/skills (Claude Code only)');
94
128
  }
95
- // 2. Claude Desktop — macOS/Windows only; ALWAYS runs, independent of the CLI.
129
+ // 3. Claude Desktop MCP bridge — macOS/Windows only; ALWAYS runs, independent
130
+ // of the CLI (skills reach Desktop via the plugin, above).
96
131
  const cfgPath = desktopPath();
97
132
  if (cfgPath !== null) {
98
133
  const entry = claudeDesktopEntry(mcpEndpoint(serverUrl), token);
@@ -104,14 +139,12 @@ export function makeClaudeCodeAdapter(env) {
104
139
  }
105
140
  },
106
141
  dryRunPlan(serverUrl, token, skills) {
107
- const lines = [];
108
- for (const s of skills) {
109
- for (const file of s.files) {
110
- lines.push(`would write ${path.join(skillsDir(), s.slug, file.path)}`);
111
- }
112
- }
142
+ // Skills plugin build + register/install.
143
+ const lines = pluginDryRunLines(pluginDir(), skills);
144
+ // MCP connector.
113
145
  const args = claudeMcpAddArgs(CONNECTOR_NAME, serverUrl, token);
114
146
  lines.push(`would run: claude ${args.join(' ')}`);
147
+ // Desktop bridge.
115
148
  const cfgPath = desktopPath();
116
149
  if (cfgPath !== null) {
117
150
  const entry = claudeDesktopEntry(mcpEndpoint(serverUrl), token);
@@ -0,0 +1,146 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { configDir } from '../config.js';
4
+ import { writeSkillTree } from '../skills.js';
5
+ /**
6
+ * Self-contained local Claude Code **plugin** built from the fetched skills, then
7
+ * registered + installed via the `claude` CLI. This is the primary skills path
8
+ * for Claude Code: a plugin installed into the shared `~/.claude/plugins/`
9
+ * registry auto-loads in BOTH the Claude Code **CLI** and the Claude **Desktop**
10
+ * app (Desktop shares that registry — the loose `~/.claude/skills/` directory is
11
+ * NOT read by Desktop), with zero manual steps and no GitHub-repo access.
12
+ *
13
+ * The plugin is fully self-contained (no symlinks, no remote source): a
14
+ * `marketplace.json` with one plugin whose `source` is `"./"` (the marketplace
15
+ * root IS the plugin), a `plugin.json`, and the skill trees under `skills/`.
16
+ */
17
+ /** Marketplace `name` — how the plugin is referenced (`<plugin>@<marketplace>`). */
18
+ export const MARKETPLACE_NAME = 'reel-edge';
19
+ /** Plugin `name` — becomes the skill namespace (`reel-edge-skills:<skill>`). */
20
+ export const PLUGIN_NAME = 'reel-edge-skills';
21
+ /** Plugin version — bump to push skill-content updates to installed users. */
22
+ export const PLUGIN_VERSION = '1.0.0';
23
+ /** `<plugin>@<marketplace>` reference used by `claude plugin install`. */
24
+ export const PLUGIN_REF = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
25
+ /** Clean, user-facing description shared by the marketplace entry + plugin.json. */
26
+ const PLUGIN_DESCRIPTION = 'Read-only domain skills for the Reel Edge MCP server: Reel Explorer (Xano ' +
27
+ 'database query guide), Inbox Scout (player emails), and HubSpot CRM.';
28
+ /**
29
+ * Absolute directory of the self-contained local plugin. Lives under the CLI's
30
+ * config home so it's stable across runs and `REELEDGE_CONFIG_DIR`-aware (tests
31
+ * point it at a temp dir).
32
+ */
33
+ export function claudePluginDir() {
34
+ return path.join(configDir(), 'claude-plugin');
35
+ }
36
+ /**
37
+ * `.claude-plugin/marketplace.json` body: the `reel-edge` marketplace with one
38
+ * plugin (`reel-edge-skills`) whose `source` is `"./"` — the marketplace root is
39
+ * the plugin itself, so no external fetch is ever needed.
40
+ */
41
+ export function marketplaceManifest() {
42
+ const body = {
43
+ $schema: 'https://anthropic.com/claude-code/marketplace.schema.json',
44
+ name: MARKETPLACE_NAME,
45
+ description: 'Reel Edge agent skills',
46
+ owner: { name: 'Reel Edge' },
47
+ plugins: [
48
+ {
49
+ name: PLUGIN_NAME,
50
+ source: './',
51
+ description: PLUGIN_DESCRIPTION,
52
+ version: PLUGIN_VERSION,
53
+ category: 'data',
54
+ },
55
+ ],
56
+ };
57
+ return `${JSON.stringify(body, null, 2)}\n`;
58
+ }
59
+ /** `.claude-plugin/plugin.json` body for the `reel-edge-skills` plugin. */
60
+ export function pluginManifest() {
61
+ const body = {
62
+ name: PLUGIN_NAME,
63
+ version: PLUGIN_VERSION,
64
+ description: PLUGIN_DESCRIPTION,
65
+ };
66
+ return `${JSON.stringify(body, null, 2)}\n`;
67
+ }
68
+ /**
69
+ * Build the self-contained plugin at `dir`:
70
+ * dir/.claude-plugin/marketplace.json
71
+ * dir/.claude-plugin/plugin.json
72
+ * dir/skills/<slug>/SKILL.md (+ references/**)
73
+ * Returns the written absolute file paths (manifests first, then skill files).
74
+ * Reuses {@link writeSkillTree}, so the same path-traversal guards apply.
75
+ */
76
+ export function buildLocalPlugin(dir, skills) {
77
+ const metaDir = path.join(dir, '.claude-plugin');
78
+ fs.mkdirSync(metaDir, { recursive: true });
79
+ const marketplacePath = path.join(metaDir, 'marketplace.json');
80
+ const pluginPath = path.join(metaDir, 'plugin.json');
81
+ fs.writeFileSync(marketplacePath, marketplaceManifest(), 'utf8');
82
+ fs.writeFileSync(pluginPath, pluginManifest(), 'utf8');
83
+ const written = [marketplacePath, pluginPath];
84
+ const skillsRoot = path.join(dir, 'skills');
85
+ for (const s of skills) {
86
+ written.push(...writeSkillTree(skillsRoot, s.slug, s.files));
87
+ }
88
+ return written;
89
+ }
90
+ /** argv for `claude plugin marketplace add <dir> --scope user`. */
91
+ export function marketplaceAddArgs(dir) {
92
+ return ['plugin', 'marketplace', 'add', dir, '--scope', 'user'];
93
+ }
94
+ /** argv for `claude plugin install reel-edge-skills@reel-edge --scope user`. */
95
+ export function pluginInstallArgs() {
96
+ return ['plugin', 'install', PLUGIN_REF, '--scope', 'user'];
97
+ }
98
+ /** True for a spawn ENOENT (the `claude` binary isn't on PATH). */
99
+ function isEnoent(err) {
100
+ return err?.code === 'ENOENT';
101
+ }
102
+ /**
103
+ * Register the local marketplace + install the plugin at **user scope** via the
104
+ * `claude` CLI, so it auto-loads in the CLI and Desktop.
105
+ *
106
+ * - **ENOENT-safe**: a missing `claude` binary short-circuits and returns
107
+ * `{ claudeFound: false }` so the caller can fall back to a loose skills write.
108
+ * - **Idempotent**: a non-ENOENT failure (marketplace/plugin already present on a
109
+ * re-run or `update`) is tolerated — same-name `marketplace add` replaces the
110
+ * prior entry, and a repeated `install` is a no-op — so re-running is safe.
111
+ */
112
+ export async function registerAndInstallPlugin(exec, dir) {
113
+ try {
114
+ await exec('claude', marketplaceAddArgs(dir));
115
+ }
116
+ catch (err) {
117
+ if (isEnoent(err))
118
+ return { claudeFound: false };
119
+ // else: tolerate (idempotent re-run) and still attempt the install.
120
+ }
121
+ try {
122
+ await exec('claude', pluginInstallArgs());
123
+ }
124
+ catch (err) {
125
+ if (isEnoent(err))
126
+ return { claudeFound: false };
127
+ // else: tolerate (already installed).
128
+ }
129
+ return { claudeFound: true };
130
+ }
131
+ /** `--dry-run` lines describing the plugin build + register/install commands. */
132
+ export function pluginDryRunLines(dir, skills) {
133
+ const lines = [
134
+ `would write ${path.join(dir, '.claude-plugin', 'marketplace.json')}`,
135
+ `would write ${path.join(dir, '.claude-plugin', 'plugin.json')}`,
136
+ ];
137
+ const skillsRoot = path.join(dir, 'skills');
138
+ for (const s of skills) {
139
+ for (const file of s.files) {
140
+ lines.push(`would write ${path.join(skillsRoot, s.slug, file.path)}`);
141
+ }
142
+ }
143
+ lines.push(`would run: claude ${marketplaceAddArgs(dir).join(' ')}`);
144
+ lines.push(`would run: claude ${pluginInstallArgs().join(' ')}`);
145
+ return lines;
146
+ }
@@ -8,15 +8,26 @@ import { APP_LABELS, CONNECTOR_NAME, } from './types.js';
8
8
  * MCP servers under `[mcp_servers.<name>]`; a remote Streamable-HTTP server is
9
9
  * indicated by a `url` key (no `transport`), and a literal `Authorization`
10
10
  * header goes in a static `http_headers` inline table. Codex also has a skills
11
- * concept — per-skill `SKILL.md` directories under `~/.agents/skills/`.
11
+ * concept — per-skill `SKILL.md` directories (with optional `references/`,
12
+ * `scripts/`, `assets/` alongside) under `~/.codex/skills/<slug>/`.
12
13
  *
13
- * Verified against https://developers.openai.com/codex/mcp,
14
- * https://developers.openai.com/codex/config-reference and
15
- * https://developers.openai.com/codex/skills (Jun 2026).
14
+ * Skills path corrected (bug fix): user/global skills load from
15
+ * `~/.codex/skills`, NOT `~/.agents/skills`. The published docs page
16
+ * (https://developers.openai.com/codex/skills) still lists `~/.agents/skills`
17
+ * as the user-level dir, but that is stale/aspirational: an openai/codex
18
+ * maintainer states skills must live in a `.codex` path for Codex to see them
19
+ * (github.com/openai/codex/discussions/9682, `.agents/skills` being only a
20
+ * proposed cross-agent standard), a regression thread confirms `~/.agents/skills`
21
+ * is no longer discovered in new sessions
22
+ * (community.openai.com/t/.../1379522), and Codex itself keeps its home under
23
+ * `~/.codex/` (config.toml, auth.json, vendor_imports/, skills/).
24
+ *
25
+ * MCP config verified against https://developers.openai.com/codex/mcp and
26
+ * https://developers.openai.com/codex/config-reference (Jun 2026).
16
27
  */
17
- /** Absolute path to the Codex skills tree. */
28
+ /** Absolute path to the Codex skills tree (`~/.codex/skills`). */
18
29
  function codexSkillsDir(env) {
19
- return path.join(env.homeDir, '.agents', 'skills');
30
+ return path.join(env.homeDir, '.codex', 'skills');
20
31
  }
21
32
  /** Absolute path to the Codex config file. */
22
33
  function codexConfigPath(env) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reeledge/agent-tools",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",