@reeledge/agent-tools 0.1.4 → 0.1.7
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/dist/apps/claude-code.js +49 -16
- package/dist/apps/claude-plugin.js +146 -0
- package/dist/apps/codex-plugin.js +152 -0
- package/dist/apps/codex.js +41 -7
- package/package.json +1 -1
package/dist/apps/claude-code.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
82
|
-
// never block the desktop
|
|
83
|
-
// "already exists" (a re-run / `update`, exit 1) or any
|
|
84
|
-
// logged as skipped, NOT thrown
|
|
85
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { writeSkillTree } from '../skills.js';
|
|
4
|
+
import { CONNECTOR_NAME } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Self-contained local Codex **app-UI plugin** built from the fetched skills and
|
|
7
|
+
* registered in the user's personal marketplace, so the Reel Edge skills surface
|
|
8
|
+
* in the Codex / ChatGPT app plugins panel (in addition to the loose
|
|
9
|
+
* `~/.codex/skills/` CLI skills + the `[mcp_servers.reel-edge]` connector the
|
|
10
|
+
* codex adapter already writes).
|
|
11
|
+
*
|
|
12
|
+
* The plugin is fully self-contained — no remote source. The exact shapes here
|
|
13
|
+
* were validated against Codex's own plugin validator and installed successfully
|
|
14
|
+
* in the app:
|
|
15
|
+
* - `<homeDir>/plugins/reel-edge/.codex-plugin/plugin.json` (manifest + app-UI `interface`)
|
|
16
|
+
* - `<homeDir>/plugins/reel-edge/skills/<slug>/…` (the fetched skill trees)
|
|
17
|
+
* - `<homeDir>/.agents/plugins/marketplace.json` (a personal marketplace whose
|
|
18
|
+
* `source.path` is the home-relative `./plugins/reel-edge`).
|
|
19
|
+
*
|
|
20
|
+
* The marketplace `source` stays the LITERAL home-relative `./plugins/reel-edge`
|
|
21
|
+
* — Codex resolves it relative to the agents home, so it must NOT be absolutised.
|
|
22
|
+
*/
|
|
23
|
+
/** Plugin `name` (also the marketplace entry name + skills namespace). */
|
|
24
|
+
export const CODEX_PLUGIN_NAME = CONNECTOR_NAME;
|
|
25
|
+
/** Plugin `version` — bump to push skill-content updates to installed users. */
|
|
26
|
+
export const CODEX_PLUGIN_VERSION = '0.1.0';
|
|
27
|
+
/** Absolute plugin directory: `<homeDir>/plugins/reel-edge`. */
|
|
28
|
+
export function codexPluginDir(env) {
|
|
29
|
+
return path.join(env.homeDir, 'plugins', CODEX_PLUGIN_NAME);
|
|
30
|
+
}
|
|
31
|
+
/** Absolute personal-marketplace path: `<homeDir>/.agents/plugins/marketplace.json`. */
|
|
32
|
+
export function codexMarketplacePath(env) {
|
|
33
|
+
return path.join(env.homeDir, '.agents', 'plugins', 'marketplace.json');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `.codex-plugin/plugin.json` body — the exact shape Codex's plugin validator
|
|
37
|
+
* accepts. `skills: "./skills/"` points at the skill trees; `interface` drives
|
|
38
|
+
* the app plugins-panel card. Pretty-printed with a trailing newline.
|
|
39
|
+
*/
|
|
40
|
+
export function codexPluginManifest() {
|
|
41
|
+
const body = {
|
|
42
|
+
name: CODEX_PLUGIN_NAME,
|
|
43
|
+
version: CODEX_PLUGIN_VERSION,
|
|
44
|
+
description: 'Reel Edge read-only data skills: Reel Explorer (Xano database), Inbox Scout (player emails), and HubSpot CRM.',
|
|
45
|
+
author: { name: 'Reel Edge' },
|
|
46
|
+
skills: './skills/',
|
|
47
|
+
interface: {
|
|
48
|
+
displayName: 'Reel Edge',
|
|
49
|
+
shortDescription: 'Reel Edge read-only data skills for Codex.',
|
|
50
|
+
longDescription: 'Adds the Reel Edge domain skills — Reel Explorer (read-only Xano SQL), Inbox Scout (player emails), and HubSpot CRM — used alongside the reel-edge MCP connector.',
|
|
51
|
+
developerName: 'Reel Edge',
|
|
52
|
+
category: 'Productivity',
|
|
53
|
+
capabilities: [],
|
|
54
|
+
defaultPrompt: 'Query the Reel Edge database.',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
return `${JSON.stringify(body, null, 2)}\n`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The reel-edge plugin entry for the personal marketplace. `source.path` is the
|
|
61
|
+
* literal home-relative `./plugins/reel-edge` (Codex resolves it against the
|
|
62
|
+
* agents home); `INSTALLED_BY_DEFAULT` makes it show up installed in the panel.
|
|
63
|
+
*/
|
|
64
|
+
export function codexMarketplaceEntry() {
|
|
65
|
+
return {
|
|
66
|
+
name: CODEX_PLUGIN_NAME,
|
|
67
|
+
source: { source: 'local', path: `./plugins/${CODEX_PLUGIN_NAME}` },
|
|
68
|
+
policy: { installation: 'INSTALLED_BY_DEFAULT', authentication: 'ON_INSTALL' },
|
|
69
|
+
category: 'Productivity',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** The fresh personal-marketplace root wrapping a single plugin entry. */
|
|
73
|
+
function freshMarketplace(entry) {
|
|
74
|
+
return {
|
|
75
|
+
name: 'personal',
|
|
76
|
+
interface: { displayName: 'Personal' },
|
|
77
|
+
plugins: [entry],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Merge the reel-edge plugin entry into an existing `marketplace.json` body (or
|
|
82
|
+
* `undefined`/malformed → a fresh personal-marketplace root). Mirrors
|
|
83
|
+
* `mergeDroidConfig`/`mergeClaudeDesktopConfig`: PRESERVE the user's top-level
|
|
84
|
+
* keys (incl. their marketplace `name`/`interface`) and any OTHER plugins;
|
|
85
|
+
* REPLACE an existing reel-edge entry in `plugins[]` (matched by `name`) or
|
|
86
|
+
* APPEND it. Returns pretty-printed JSON with a trailing newline.
|
|
87
|
+
*/
|
|
88
|
+
export function mergeCodexMarketplace(existing, entry) {
|
|
89
|
+
let root = null;
|
|
90
|
+
if (existing && existing.trim() !== '') {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(existing);
|
|
93
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
94
|
+
root = parsed;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
root = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (root === null) {
|
|
102
|
+
return `${JSON.stringify(freshMarketplace(entry), null, 2)}\n`;
|
|
103
|
+
}
|
|
104
|
+
const plugins = Array.isArray(root.plugins) ? [...root.plugins] : [];
|
|
105
|
+
const idx = plugins.findIndex((p) => p !== null &&
|
|
106
|
+
typeof p === 'object' &&
|
|
107
|
+
p.name === entry.name);
|
|
108
|
+
if (idx >= 0)
|
|
109
|
+
plugins[idx] = entry;
|
|
110
|
+
else
|
|
111
|
+
plugins.push(entry);
|
|
112
|
+
root.plugins = plugins;
|
|
113
|
+
// Don't clobber a user's marketplace name/interface; only default them when
|
|
114
|
+
// absent so the merged file is still a valid personal marketplace.
|
|
115
|
+
if (root.name === undefined)
|
|
116
|
+
root.name = 'personal';
|
|
117
|
+
if (root.interface === undefined)
|
|
118
|
+
root.interface = { displayName: 'Personal' };
|
|
119
|
+
return `${JSON.stringify(root, null, 2)}\n`;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build the Codex plugin at `dir`:
|
|
123
|
+
* dir/.codex-plugin/plugin.json
|
|
124
|
+
* dir/skills/<slug>/SKILL.md (+ references/**)
|
|
125
|
+
* Copies the fetched `SkillFile[]` verbatim (frontmatter carried through) via
|
|
126
|
+
* {@link writeSkillTree}, so the same path-traversal guards apply. Returns the
|
|
127
|
+
* written absolute file paths (the manifest first, then the skill files).
|
|
128
|
+
*/
|
|
129
|
+
export function buildCodexPlugin(dir, skills) {
|
|
130
|
+
const metaDir = path.join(dir, '.codex-plugin');
|
|
131
|
+
fs.mkdirSync(metaDir, { recursive: true });
|
|
132
|
+
const pluginPath = path.join(metaDir, 'plugin.json');
|
|
133
|
+
fs.writeFileSync(pluginPath, codexPluginManifest(), 'utf8');
|
|
134
|
+
const written = [pluginPath];
|
|
135
|
+
const skillsRoot = path.join(dir, 'skills');
|
|
136
|
+
for (const s of skills) {
|
|
137
|
+
written.push(...writeSkillTree(skillsRoot, s.slug, s.files));
|
|
138
|
+
}
|
|
139
|
+
return written;
|
|
140
|
+
}
|
|
141
|
+
/** `--dry-run` lines describing the plugin build + the marketplace merge. */
|
|
142
|
+
export function codexPluginDryRunLines(dir, marketplacePath, skills) {
|
|
143
|
+
const lines = [`would write ${path.join(dir, '.codex-plugin', 'plugin.json')}`];
|
|
144
|
+
const skillsRoot = path.join(dir, 'skills');
|
|
145
|
+
for (const s of skills) {
|
|
146
|
+
for (const file of s.files) {
|
|
147
|
+
lines.push(`would write ${path.join(skillsRoot, s.slug, file.path)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
lines.push(`would merge ${marketplacePath} with plugin entry "${CODEX_PLUGIN_NAME}" (INSTALLED_BY_DEFAULT)`);
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
package/dist/apps/codex.js
CHANGED
|
@@ -2,21 +2,39 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { mcpEndpoint } from '../mcp.js';
|
|
4
4
|
import { writeSkillTree } from '../skills.js';
|
|
5
|
+
import { buildCodexPlugin, codexMarketplaceEntry, codexMarketplacePath, codexPluginDir, codexPluginDryRunLines, mergeCodexMarketplace, } from './codex-plugin.js';
|
|
5
6
|
import { APP_LABELS, CONNECTOR_NAME, } from './types.js';
|
|
6
7
|
/**
|
|
7
8
|
* OpenAI Codex CLI adapter. Codex reads `~/.codex/config.toml` and registers
|
|
8
9
|
* MCP servers under `[mcp_servers.<name>]`; a remote Streamable-HTTP server is
|
|
9
10
|
* indicated by a `url` key (no `transport`), and a literal `Authorization`
|
|
10
11
|
* header goes in a static `http_headers` inline table. Codex also has a skills
|
|
11
|
-
* concept — per-skill `SKILL.md` directories
|
|
12
|
+
* concept — per-skill `SKILL.md` directories (with optional `references/`,
|
|
13
|
+
* `scripts/`, `assets/` alongside) under `~/.codex/skills/<slug>/`.
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* https://developers.openai.com/codex/skills
|
|
15
|
+
* Skills path corrected (bug fix): user/global skills load from
|
|
16
|
+
* `~/.codex/skills`, NOT `~/.agents/skills`. The published docs page
|
|
17
|
+
* (https://developers.openai.com/codex/skills) still lists `~/.agents/skills`
|
|
18
|
+
* as the user-level dir, but that is stale/aspirational: an openai/codex
|
|
19
|
+
* maintainer states skills must live in a `.codex` path for Codex to see them
|
|
20
|
+
* (github.com/openai/codex/discussions/9682, `.agents/skills` being only a
|
|
21
|
+
* proposed cross-agent standard), a regression thread confirms `~/.agents/skills`
|
|
22
|
+
* is no longer discovered in new sessions
|
|
23
|
+
* (community.openai.com/t/.../1379522), and Codex itself keeps its home under
|
|
24
|
+
* `~/.codex/` (config.toml, auth.json, vendor_imports/, skills/).
|
|
25
|
+
*
|
|
26
|
+
* MCP config verified against https://developers.openai.com/codex/mcp and
|
|
27
|
+
* https://developers.openai.com/codex/config-reference (Jun 2026).
|
|
28
|
+
*
|
|
29
|
+
* On top of the loose CLI skills + the MCP connector, this adapter also builds a
|
|
30
|
+
* Codex app-UI **plugin** (see `./codex-plugin.ts`) — a self-contained plugin
|
|
31
|
+
* under `<home>/plugins/reel-edge/` registered in the personal marketplace
|
|
32
|
+
* (`<home>/.agents/plugins/marketplace.json`) so the skills show in the
|
|
33
|
+
* Codex/ChatGPT app plugins panel.
|
|
16
34
|
*/
|
|
17
|
-
/** Absolute path to the Codex skills tree. */
|
|
35
|
+
/** Absolute path to the Codex skills tree (`~/.codex/skills`). */
|
|
18
36
|
function codexSkillsDir(env) {
|
|
19
|
-
return path.join(env.homeDir, '.
|
|
37
|
+
return path.join(env.homeDir, '.codex', 'skills');
|
|
20
38
|
}
|
|
21
39
|
/** Absolute path to the Codex config file. */
|
|
22
40
|
function codexConfigPath(env) {
|
|
@@ -95,15 +113,29 @@ export function makeCodexAdapter(env) {
|
|
|
95
113
|
label: APP_LABELS.codex,
|
|
96
114
|
skillsDir,
|
|
97
115
|
writeSkills(skills) {
|
|
98
|
-
|
|
116
|
+
// 1. Loose CLI skills under ~/.codex/skills/<slug>/ (existing behaviour).
|
|
117
|
+
const cliSkills = skills.flatMap((s) => writeSkillTree(skillsDir(), s.slug, s.files));
|
|
118
|
+
// 2. The Codex app-UI plugin under <home>/plugins/reel-edge/ so the skills
|
|
119
|
+
// also surface in the Codex/ChatGPT app plugins panel.
|
|
120
|
+
const plugin = buildCodexPlugin(codexPluginDir(env), skills);
|
|
121
|
+
return [...cliSkills, ...plugin];
|
|
99
122
|
},
|
|
100
123
|
async configureMcp(serverUrl, token) {
|
|
124
|
+
// 1. MCP connector: ~/.codex/config.toml [mcp_servers.reel-edge] (existing).
|
|
101
125
|
const block = codexConfigBlock(CONNECTOR_NAME, mcpEndpoint(serverUrl), token);
|
|
102
126
|
const cfgPath = codexConfigPath(env);
|
|
103
127
|
const existing = fs.existsSync(cfgPath) ? fs.readFileSync(cfgPath, 'utf8') : '';
|
|
104
128
|
const merged = mergeCodexToml(existing, CONNECTOR_NAME, block);
|
|
105
129
|
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
106
130
|
fs.writeFileSync(cfgPath, merged, 'utf8');
|
|
131
|
+
// 2. Register the app-UI plugin in the personal marketplace so it installs
|
|
132
|
+
// in the Codex app plugins panel.
|
|
133
|
+
const mpPath = codexMarketplacePath(env);
|
|
134
|
+
const existingMp = fs.existsSync(mpPath) ? fs.readFileSync(mpPath, 'utf8') : undefined;
|
|
135
|
+
const mergedMp = mergeCodexMarketplace(existingMp, codexMarketplaceEntry());
|
|
136
|
+
fs.mkdirSync(path.dirname(mpPath), { recursive: true });
|
|
137
|
+
fs.writeFileSync(mpPath, mergedMp, 'utf8');
|
|
138
|
+
env.log?.(`Registered Codex plugin (${CONNECTOR_NAME}) — installs in the Codex app plugins panel`);
|
|
107
139
|
},
|
|
108
140
|
dryRunPlan(serverUrl, token, skills) {
|
|
109
141
|
const lines = [];
|
|
@@ -116,6 +148,8 @@ export function makeCodexAdapter(env) {
|
|
|
116
148
|
for (const l of codexConfigBlock(CONNECTOR_NAME, mcpEndpoint(serverUrl), token).trimEnd().split('\n')) {
|
|
117
149
|
lines.push(` ${l}`);
|
|
118
150
|
}
|
|
151
|
+
// The Codex app-UI plugin build + the personal-marketplace registration.
|
|
152
|
+
lines.push(...codexPluginDryRunLines(codexPluginDir(env), codexMarketplacePath(env), skills));
|
|
119
153
|
return lines;
|
|
120
154
|
},
|
|
121
155
|
};
|
package/package.json
CHANGED