@kiran_nandi_123/conxa 1.0.0

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/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # conxa
2
+
3
+ CLI for installing and running Conxa automation plugins on top of the
4
+ shared `conxa` MCP runtime.
5
+
6
+ ## Install plugins
7
+
8
+ ```
9
+ npx -y conxa install <plugin_id>
10
+ ```
11
+
12
+ Plugin refs accepted:
13
+
14
+ - `acme/hr-onboarding` — GitHub `owner/repo` (cloned via git)
15
+ - `acme/hr-onboarding@v1.0.0` — pinned version (git tag)
16
+ - `https://github.com/acme/hr-onboarding` — full git URL
17
+ - `./my-plugin` — local directory
18
+
19
+ The first install bootstraps `~/.conxa/runtime/`, registers the `conxa`
20
+ MCP server in `~/.claude/settings.json`, and imports the per-plugin
21
+ CLAUDE.md instructions into your global `~/.claude/CLAUDE.md`. After
22
+ that, Claude Code discovers every installed plugin through a single MCP
23
+ connection — no per-plugin setup.
24
+
25
+ ## Other commands
26
+
27
+ ```
28
+ conxa list # list installed plugins
29
+ conxa search <query> # search installed + cached + registry plugins
30
+ conxa uninstall <slug> # remove an installed plugin
31
+ conxa init # explicit runtime bootstrap (auto-runs on install)
32
+ conxa registry login <url> <token>
33
+ conxa registry logout <url>
34
+ ```
35
+
36
+ ## Architecture
37
+
38
+ All installed plugins share one MCP server (`conxa`), one Chromium auth
39
+ cache, and one runtime install. Plugins themselves are data-only
40
+ packages — recorded workflows compiled into deterministic
41
+ `execution.json` + `recovery.json` files, with a 5-layer recovery
42
+ cascade implemented inside the runtime.
43
+
44
+ See https://conxa.ai for the broader platform.
package/bin/conxa.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // Thin shim — all logic lives in lib/cli.js. The same cli.js is also installed
4
+ // into ~/.conxa/runtime/ by `conxa init` so the in-runtime server.js can shell
5
+ // to it directly without needing the npm bin on $PATH.
6
+ const { runCli } = require("../lib/cli.js");
7
+ runCli(process.argv.slice(2));
package/lib/browser.js ADDED
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ const { chromium } = require("playwright");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { getPluginConfig, getPluginDir, getAuthJson } = require("./config");
6
+
7
+ // ─── Browser cache (per-slug, 5-min idle timeout) ────────────────────────────
8
+
9
+ const _cache = new Map(); // slug → { browser, context, idleTimer }
10
+ const BROWSER_IDLE_MS = 5 * 60 * 1000;
11
+
12
+ function _scheduleCleanup(slug) {
13
+ const entry = _cache.get(slug);
14
+ if (!entry) return;
15
+ clearTimeout(entry.idleTimer);
16
+ entry.idleTimer = setTimeout(async () => {
17
+ console.error(`[browser-cache] Idle timeout for ${slug} — closing browser`);
18
+ const b = entry.browser;
19
+ _cache.delete(slug);
20
+ if (b) await b.close().catch(() => {});
21
+ }, BROWSER_IDLE_MS);
22
+ }
23
+
24
+ async function getCachedBrowser(slug) {
25
+ const entry = _cache.get(slug);
26
+ if (entry && entry.browser && entry.context) {
27
+ try {
28
+ entry.context.pages(); // throws if context is closed
29
+ _scheduleCleanup(slug);
30
+ console.error(`[browser-cache] Reusing cached browser for ${slug}`);
31
+ return { browser: entry.browser, context: entry.context, cached: true };
32
+ } catch (_) {
33
+ _cache.delete(slug);
34
+ }
35
+ }
36
+ const { browser, context } = await getAuthContext(slug, false);
37
+ _cache.set(slug, { browser, context, idleTimer: null });
38
+ _scheduleCleanup(slug);
39
+ console.error(`[browser-cache] Launched new browser for ${slug}`);
40
+ return { browser, context, cached: false };
41
+ }
42
+
43
+ // ─── Session management ───────────────────────────────────────────────────────
44
+
45
+ function isAuthenticated(page, protectedUrl) {
46
+ try {
47
+ const u = new URL(page.url());
48
+ return u.hostname === new URL(protectedUrl).hostname && !u.pathname.startsWith("/login");
49
+ } catch (_) { return false; }
50
+ }
51
+
52
+ async function getAuthContext(slug, headless) {
53
+ const cfg = getPluginConfig(slug);
54
+ const protectedUrl = cfg.protected_url;
55
+ const targetUrl = cfg.target_url;
56
+ const authJson = getAuthJson(slug);
57
+
58
+ if (fs.existsSync(authJson)) {
59
+ let stored;
60
+ try { stored = JSON.parse(fs.readFileSync(authJson, "utf8")); } catch (_) {}
61
+ if (stored) {
62
+ const browser = await chromium.launch({ headless: headless !== false });
63
+ const context = await browser.newContext({ storageState: stored });
64
+ const page = await context.newPage();
65
+ await page.goto(protectedUrl, { waitUntil: "domcontentloaded", timeout: 30000 }).catch(() => {});
66
+ await page.waitForTimeout(1500);
67
+ if (isAuthenticated(page, protectedUrl)) {
68
+ await page.close();
69
+ console.error(`[auth:${slug}] Session restored from auth.json`);
70
+ return { browser, context };
71
+ }
72
+ await browser.close();
73
+ console.error(`[auth:${slug}] Stored session expired — starting manual login`);
74
+ }
75
+ } else {
76
+ console.error(`[auth:${slug}] No auth.json — starting manual login`);
77
+ }
78
+
79
+ console.error(`[auth:${slug}] Opening login browser — waiting for user to authenticate...`);
80
+ const loginBrowser = await chromium.launch({ headless: false });
81
+ const loginCtx = await loginBrowser.newContext();
82
+ const loginPage = await loginCtx.newPage();
83
+ await loginPage.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
84
+ try {
85
+ await loginPage.waitForURL(
86
+ url => url.href.startsWith(protectedUrl) && !url.href.includes("/login"),
87
+ { timeout: 300000 }
88
+ );
89
+ } catch (_) {
90
+ await loginBrowser.close();
91
+ throw new Error("Authentication timed out after 5 minutes. Please try again.");
92
+ }
93
+
94
+ const state = await loginCtx.storageState();
95
+ fs.mkdirSync(path.dirname(authJson), { recursive: true });
96
+ fs.writeFileSync(authJson, JSON.stringify(state, null, 2));
97
+ console.error(`[auth:${slug}] Session saved to auth.json — closing login browser`);
98
+ await loginBrowser.close();
99
+
100
+ console.error(`[auth:${slug}] Relaunching authenticated browser...`);
101
+ const browser = await chromium.launch({ headless: headless !== false });
102
+ const context = await browser.newContext({ storageState: state });
103
+ const page = await context.newPage();
104
+ await page.goto(protectedUrl, { waitUntil: "domcontentloaded", timeout: 30000 }).catch(() => {});
105
+ await page.waitForTimeout(1500);
106
+ if (!isAuthenticated(page, protectedUrl)) {
107
+ await browser.close();
108
+ throw new Error("Authenticated navigation failed after login — unexpected error.");
109
+ }
110
+ await page.close();
111
+ console.error(`[auth:${slug}] Authenticated context ready`);
112
+ return { browser, context };
113
+ }
114
+
115
+ // ─── Graceful shutdown ────────────────────────────────────────────────────────
116
+
117
+ async function gracefulShutdown() {
118
+ for (const [slug, entry] of _cache.entries()) {
119
+ clearTimeout(entry.idleTimer);
120
+ if (entry.browser) {
121
+ console.error(`[browser-cache] SIGINT/SIGTERM — closing browser for ${slug}`);
122
+ await entry.browser.close().catch(() => {});
123
+ }
124
+ }
125
+ _cache.clear();
126
+ process.exit(0);
127
+ }
128
+
129
+ module.exports = { getCachedBrowser, isAuthenticated, getAuthContext, gracefulShutdown };
package/lib/cli.js ADDED
@@ -0,0 +1,398 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * cli.js — Conxa runtime manager
5
+ *
6
+ * Commands:
7
+ * init Bootstrap ~/.conxa/runtime/ (idempotent)
8
+ * install <ref> Install a plugin. <ref> is a local dir, "owner/repo",
9
+ * "owner/repo@v1.0.0", or an https git URL. Resolver
10
+ * chain: installed → cache → git → registry.
11
+ * uninstall <slug> Remove an installed plugin
12
+ * list Print all installed plugins
13
+ * search <query> Search installed + cached + registry plugins
14
+ * registry login <url> <tok> Save credentials for a private registry
15
+ * registry logout <url> Remove credentials for a registry
16
+ */
17
+ const fs = require("fs");
18
+ const os = require("os");
19
+ const path = require("path");
20
+ const { execSync } = require("child_process");
21
+
22
+ const CONXA_HOME = path.join(os.homedir(), ".conxa");
23
+ const RUNTIME_DIR = path.join(CONXA_HOME, "runtime");
24
+ const PLUGINS_DIR = path.join(CONXA_HOME, "plugins");
25
+ const REGISTRY_PATH = path.join(CONXA_HOME, "registry.json");
26
+ const CONXA_CLAUDE_MD = path.join(CONXA_HOME, "CLAUDE.md");
27
+ const CONXA_INDEX_MD = path.join(CONXA_HOME, "index.md");
28
+ const VERSION_JSON = path.join(RUNTIME_DIR, "version.json");
29
+ const SETTINGS_JSON = path.join(os.homedir(), ".claude", "settings.json");
30
+ const GLOBAL_CLAUDE_MD = path.join(os.homedir(), ".claude", "CLAUDE.md");
31
+ const SERVER_JS = path.join(RUNTIME_DIR, "server.js");
32
+
33
+ // ─── Registry helpers ─────────────────────────────────────────────────────────
34
+
35
+ function readRegistry() {
36
+ if (!fs.existsSync(REGISTRY_PATH)) return {};
37
+ try { return JSON.parse(fs.readFileSync(REGISTRY_PATH, "utf8")); } catch (_) { return {}; }
38
+ }
39
+
40
+ function writeRegistry(reg) {
41
+ fs.mkdirSync(CONXA_HOME, { recursive: true });
42
+ fs.writeFileSync(REGISTRY_PATH, JSON.stringify(reg, null, 2));
43
+ }
44
+
45
+ // ─── Claude Code integration ─────────────────────────────────────────────────
46
+
47
+ function _registerGlobalMcp() {
48
+ let settings = {};
49
+ try { settings = JSON.parse(fs.readFileSync(SETTINGS_JSON, "utf8")); } catch (_) {}
50
+ const existing = settings.mcpServers && settings.mcpServers.conxa;
51
+ if (existing && existing.args && existing.args[0] === SERVER_JS) return;
52
+ if (!settings.mcpServers) settings.mcpServers = {};
53
+ settings.mcpServers.conxa = { command: "node", args: [SERVER_JS] };
54
+ try {
55
+ fs.mkdirSync(path.dirname(SETTINGS_JSON), { recursive: true });
56
+ fs.writeFileSync(SETTINGS_JSON, JSON.stringify(settings, null, 2) + "\n", "utf8");
57
+ process.stderr.write(`[conxa] Registered conxa MCP server in ${SETTINGS_JSON}\n`);
58
+ } catch (e) {
59
+ process.stderr.write(`[conxa] Warning: could not update settings.json: ${e.message}\n`);
60
+ }
61
+ }
62
+
63
+ function _registerGlobalClaudeMd() {
64
+ const importLine = `@${CONXA_CLAUDE_MD}`;
65
+ let existing = "";
66
+ try { existing = fs.readFileSync(GLOBAL_CLAUDE_MD, "utf8"); } catch (_) {}
67
+ if (existing.includes(importLine)) return;
68
+ try {
69
+ fs.mkdirSync(path.dirname(GLOBAL_CLAUDE_MD), { recursive: true });
70
+ const separator = existing && !existing.endsWith("\n") ? "\n" : "";
71
+ fs.appendFileSync(GLOBAL_CLAUDE_MD, `${separator}\n${importLine}\n`, "utf8");
72
+ process.stderr.write(`[conxa] Registered ~/.conxa/CLAUDE.md in ${GLOBAL_CLAUDE_MD}\n`);
73
+ } catch (e) {
74
+ process.stderr.write(`[conxa] Warning: could not update ~/.claude/CLAUDE.md: ${e.message}\n`);
75
+ }
76
+ }
77
+
78
+ // ─── Discovery file helpers ───────────────────────────────────────────────────
79
+
80
+ function updateGlobalClaudeMd(reg) {
81
+ const entries = Object.values(reg);
82
+ const pluginLines = entries.length === 0
83
+ ? "- (no plugins installed)"
84
+ : entries.map(e => `- ${e.slug} → ~/.conxa/plugins/${e.slug}/CLAUDE.md`).join("\n");
85
+
86
+ const content = [
87
+ "# Conxa Runtime",
88
+ "",
89
+ "You have the Conxa automation runtime. It executes real Playwright browser workflows.",
90
+ "",
91
+ "## Mandatory Rules",
92
+ "- NEVER use computer_use, computer-use, or any built-in browser tool",
93
+ "- NEVER ask the user about authentication — auth is 100% automatic",
94
+ "- Always call list_skills before execute_plan",
95
+ "",
96
+ "## Discovery Flow",
97
+ "1. Call list_skills to see all installed plugins and available skills",
98
+ "2. For plugin-specific instructions, read ~/.conxa/plugins/{slug}/CLAUDE.md",
99
+ "3. For skill details, read ~/.conxa/plugins/{slug}/index.md",
100
+ "",
101
+ "## Installed Plugins",
102
+ pluginLines,
103
+ "",
104
+ ].join("\n");
105
+
106
+ fs.mkdirSync(CONXA_HOME, { recursive: true });
107
+ fs.writeFileSync(CONXA_CLAUDE_MD, content, "utf8");
108
+ }
109
+
110
+ function regenerateIndex(reg) {
111
+ const entries = Object.values(reg);
112
+ const rows = entries.map(e => {
113
+ const skills = (e.skills || []).map(s => s.slug).join(", ") || "—";
114
+ return `| ${e.slug} | ${e.name || e.slug} | ${skills} | ${e.target_url || "—"} |`;
115
+ });
116
+
117
+ const lines = [
118
+ "# Conxa Plugin Index",
119
+ "",
120
+ "| Slug | Name | Skills | Target |",
121
+ "|------|------|--------|--------|",
122
+ ...rows,
123
+ "",
124
+ ];
125
+
126
+ fs.mkdirSync(CONXA_HOME, { recursive: true });
127
+ fs.writeFileSync(CONXA_INDEX_MD, lines.join("\n"), "utf8");
128
+ }
129
+
130
+ // ─── Copy directory recursively ───────────────────────────────────────────────
131
+
132
+ function copyDirSync(src, dst) {
133
+ fs.mkdirSync(dst, { recursive: true });
134
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
135
+ const s = path.join(src, entry.name);
136
+ const d = path.join(dst, entry.name);
137
+ if (entry.isDirectory()) copyDirSync(s, d);
138
+ else fs.copyFileSync(s, d);
139
+ }
140
+ }
141
+
142
+ // ─── init ─────────────────────────────────────────────────────────────────────
143
+
144
+ function init() {
145
+ if (fs.existsSync(VERSION_JSON)) {
146
+ const v = JSON.parse(fs.readFileSync(VERSION_JSON, "utf8"));
147
+ process.stderr.write(`[conxa] Runtime already bootstrapped at ${RUNTIME_DIR} (v${v.version})\n`);
148
+ return;
149
+ }
150
+ process.stderr.write(`[conxa] Bootstrapping runtime at ${RUNTIME_DIR} ...\n`);
151
+
152
+ // All runtime files live alongside cli.js in both layouts: the in-repo
153
+ // template tree (app/storage/plugin_templates/runtime/) and the published
154
+ // npm package (packages/conxa-cli/lib/). Copy the whole directory so new
155
+ // files (resolver/, search.js, etc.) ship automatically without having to
156
+ // maintain a hardcoded allow-list.
157
+ fs.mkdirSync(RUNTIME_DIR, { recursive: true });
158
+ for (const entry of fs.readdirSync(__dirname, { withFileTypes: true })) {
159
+ if (entry.name === "node_modules" || entry.name === ".bootstrapped") continue;
160
+ const src = path.join(__dirname, entry.name);
161
+ const dst = path.join(RUNTIME_DIR, entry.name);
162
+ if (entry.isDirectory()) copyDirSync(src, dst);
163
+ else fs.copyFileSync(src, dst);
164
+ }
165
+
166
+ process.stderr.write("[conxa] Running npm install...\n");
167
+ execSync("npm install --prefer-offline --silent", { cwd: RUNTIME_DIR, stdio: ["ignore", "pipe", "inherit"] });
168
+
169
+ process.stderr.write("[conxa] Installing Playwright Chromium...\n");
170
+ execSync("npx playwright install chromium", { cwd: RUNTIME_DIR, stdio: ["ignore", "pipe", "inherit"] });
171
+
172
+ const pkg = fs.existsSync(path.join(RUNTIME_DIR, "package.json"))
173
+ ? JSON.parse(fs.readFileSync(path.join(RUNTIME_DIR, "package.json"), "utf8"))
174
+ : {};
175
+ fs.writeFileSync(VERSION_JSON, JSON.stringify({
176
+ version: pkg.version || "1.0.0",
177
+ installed_at: new Date().toISOString(),
178
+ node_version: process.version,
179
+ }, null, 2));
180
+
181
+ // Write initial global CLAUDE.md and index.md with empty registry
182
+ updateGlobalClaudeMd({});
183
+ regenerateIndex({});
184
+
185
+ // Register the shared MCP server + global CLAUDE.md import so Claude Code
186
+ // picks up `conxa` and per-plugin instructions on its next launch.
187
+ _registerGlobalMcp();
188
+ _registerGlobalClaudeMd();
189
+
190
+ // Write bootstrap flag so subsequent plugin installs skip re-init
191
+ fs.writeFileSync(path.join(CONXA_HOME, ".bootstrapped"), "1", "utf8");
192
+
193
+ process.stderr.write("[conxa] Bootstrap complete.\n");
194
+ }
195
+
196
+ // ─── install ──────────────────────────────────────────────────────────────────
197
+
198
+ function _installFromLocalDir(pluginDir) {
199
+ if (!pluginDir) throw new Error("install: plugin directory path required");
200
+ const absDir = path.resolve(pluginDir);
201
+ if (!fs.existsSync(absDir)) throw new Error(`Plugin directory not found: ${absDir}`);
202
+
203
+ const cfgPath = path.join(absDir, "plugin.json");
204
+ if (!fs.existsSync(cfgPath)) throw new Error(`No plugin.json found in ${absDir}`);
205
+
206
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
207
+ if (!cfg.slug) throw new Error("plugin.json missing: slug");
208
+ if (!cfg.target_url) throw new Error("plugin.json missing: target_url");
209
+ if (!cfg.protected_url) throw new Error("plugin.json missing: protected_url");
210
+
211
+ const slug = cfg.slug;
212
+ const destDir = path.join(PLUGINS_DIR, slug);
213
+
214
+ process.stderr.write(`[conxa] Installing plugin '${slug}' from ${absDir}...\n`);
215
+ fs.mkdirSync(destDir, { recursive: true });
216
+
217
+ // Copy plugin manifest
218
+ fs.copyFileSync(cfgPath, path.join(destDir, "plugin.json"));
219
+
220
+ // Copy discovery files
221
+ for (const name of ["CLAUDE.md", "index.md", "schema.json", "README.md"]) {
222
+ const src = path.join(absDir, name);
223
+ if (fs.existsSync(src)) fs.copyFileSync(src, path.join(destDir, name));
224
+ }
225
+
226
+ // Copy skills/
227
+ const skillsSrc = path.join(absDir, "skills");
228
+ if (fs.existsSync(skillsSrc)) copyDirSync(skillsSrc, path.join(destDir, "skills"));
229
+
230
+ // Copy auth/credentials.example.json (never auth.json)
231
+ const credsEx = path.join(absDir, "auth", "credentials.example.json");
232
+ if (fs.existsSync(credsEx)) {
233
+ fs.mkdirSync(path.join(destDir, "auth"), { recursive: true });
234
+ fs.copyFileSync(credsEx, path.join(destDir, "auth", "credentials.example.json"));
235
+ }
236
+
237
+ // Update master registry
238
+ const skillsList = (cfg.skills || []).map(s => ({ slug: s.slug, path: s.path || `skills/${s.slug}` }));
239
+ const entry = {
240
+ slug,
241
+ name: cfg.name,
242
+ version: cfg.version || "1.0.0",
243
+ path: destDir,
244
+ target_url: cfg.target_url,
245
+ protected_url: cfg.protected_url,
246
+ skills: skillsList,
247
+ installed_at: new Date().toISOString(),
248
+ };
249
+ const reg = readRegistry();
250
+ reg[slug] = entry;
251
+ writeRegistry(reg);
252
+
253
+ // Regenerate global discovery files
254
+ updateGlobalClaudeMd(reg);
255
+ regenerateIndex(reg);
256
+
257
+ process.stderr.write(`[conxa] Plugin '${slug}' installed. Skills: ${skillsList.map(s => s.slug).join(", ")}\n`);
258
+ return entry;
259
+ }
260
+
261
+ function _ensureInitialized() {
262
+ if (fs.existsSync(VERSION_JSON)) return;
263
+ init();
264
+ }
265
+
266
+ // `install <ref>` — accept a local dir, git ref, or registry plugin_id.
267
+ // Local dirs install directly; everything else resolves through the chain
268
+ // (cache → git → registry) which stages a directory under ~/.conxa/cache/
269
+ // before delegating to _installFromLocalDir.
270
+ async function install(ref) {
271
+ if (!ref) throw new Error("install: <ref> required (local dir, owner/repo, or plugin_id)");
272
+ _ensureInitialized();
273
+ if (fs.existsSync(ref) && fs.statSync(ref).isDirectory()) {
274
+ return _installFromLocalDir(ref);
275
+ }
276
+ // Look in cache first (already downloaded). cache.stagedDir() takes a
277
+ // plugin_id+version pair; we accept either "id" or "id@version".
278
+ const at = ref.lastIndexOf("@");
279
+ const id = at > 0 ? ref.slice(0, at) : ref;
280
+ const ver = at > 0 ? ref.slice(at + 1) : null;
281
+ const cache = require("./resolver/cache");
282
+ const staged = cache.stagedDir(id, ver);
283
+ if (staged) return _installFromLocalDir(staged);
284
+ // git resolver handles owner/repo and full https URLs. It stages into cache/
285
+ // and returns the staged directory path.
286
+ const git = require("./resolver/git");
287
+ const resolved = await git.resolve(ref);
288
+ if (resolved && resolved.staged_dir) return _installFromLocalDir(resolved.staged_dir);
289
+ // Registry resolver is contract-only today; falls through when no hosted
290
+ // registry is configured. When implemented it would download a tarball into
291
+ // cache/ and return the staged path.
292
+ throw new Error(`install: could not resolve '${ref}'`);
293
+ }
294
+
295
+ // ─── search ───────────────────────────────────────────────────────────────────
296
+
297
+ async function search(query) {
298
+ const results = await require("./search").search(query, 20);
299
+ if (results.length === 0) {
300
+ process.stderr.write(`[conxa] No matches for '${query}'.\n`);
301
+ return [];
302
+ }
303
+ for (const r of results) {
304
+ const tags = (r.tags || []).join(",") || "—";
305
+ process.stderr.write(` ${r.plugin_id || r.slug} v${r.version} [${r.source}] ${r.name} (tags: ${tags})\n`);
306
+ }
307
+ return results;
308
+ }
309
+
310
+ // ─── registry login / logout ──────────────────────────────────────────────────
311
+
312
+ function registryLogin(url, token, name) {
313
+ if (!url || !token) throw new Error("registry login: <url> <token> required");
314
+ const { getRegistryAuth, writeRegistryAuth } = require("./config");
315
+ const auth = getRegistryAuth();
316
+ const regs = Array.isArray(auth.registries) ? auth.registries : [];
317
+ const idx = regs.findIndex(r => r.url === url);
318
+ const entry = { name: name || url, url, token };
319
+ if (idx >= 0) regs[idx] = entry; else regs.push(entry);
320
+ writeRegistryAuth({ registries: regs });
321
+ process.stderr.write(`[conxa] Saved credentials for ${url}\n`);
322
+ }
323
+
324
+ function registryLogout(url) {
325
+ if (!url) throw new Error("registry logout: <url> required");
326
+ const { getRegistryAuth, writeRegistryAuth } = require("./config");
327
+ const auth = getRegistryAuth();
328
+ const regs = Array.isArray(auth.registries) ? auth.registries.filter(r => r.url !== url) : [];
329
+ writeRegistryAuth({ registries: regs });
330
+ process.stderr.write(`[conxa] Removed credentials for ${url}\n`);
331
+ }
332
+
333
+ // ─── uninstall ────────────────────────────────────────────────────────────────
334
+
335
+ function uninstall(slug) {
336
+ if (!slug) throw new Error("uninstall: slug required");
337
+ const destDir = path.join(PLUGINS_DIR, slug);
338
+ if (fs.existsSync(destDir)) {
339
+ fs.rmSync(destDir, { recursive: true, force: true });
340
+ process.stderr.write(`[conxa] Removed plugin directory: ${destDir}\n`);
341
+ }
342
+ const reg = readRegistry();
343
+ if (reg[slug]) {
344
+ delete reg[slug];
345
+ writeRegistry(reg);
346
+ updateGlobalClaudeMd(reg);
347
+ regenerateIndex(reg);
348
+ process.stderr.write(`[conxa] Removed '${slug}' from registry\n`);
349
+ } else {
350
+ process.stderr.write(`[conxa] Plugin '${slug}' was not in registry\n`);
351
+ }
352
+ }
353
+
354
+ // ─── list ─────────────────────────────────────────────────────────────────────
355
+
356
+ function list() {
357
+ const reg = readRegistry();
358
+ const entries = Object.values(reg);
359
+ if (entries.length === 0) {
360
+ process.stderr.write("[conxa] No plugins installed.\n");
361
+ return;
362
+ }
363
+ for (const e of entries) {
364
+ process.stderr.write(` ${e.slug} v${e.version} skills: ${(e.skills || []).map(s => s.slug).join(", ")}\n`);
365
+ }
366
+ }
367
+
368
+ // ─── CLI entry point ──────────────────────────────────────────────────────────
369
+
370
+ async function runCli(argv) {
371
+ const [cmd, ...rest] = argv;
372
+ try {
373
+ switch (cmd) {
374
+ case "init": init(); break;
375
+ case "install": await install(rest[0]); break;
376
+ case "uninstall": uninstall(rest[0]); break;
377
+ case "list": list(); break;
378
+ case "search": await search(rest.join(" ")); break;
379
+ case "registry":
380
+ if (rest[0] === "login") registryLogin(rest[1], rest[2], rest[3]);
381
+ else if (rest[0] === "logout") registryLogout(rest[1]);
382
+ else throw new Error("registry: login <url> <token> | logout <url>");
383
+ break;
384
+ default:
385
+ process.stderr.write("Usage: conxa <init|install <ref>|uninstall <slug>|list|search <q>|registry login|registry logout>\n");
386
+ process.exit(1);
387
+ }
388
+ } catch (e) {
389
+ process.stderr.write(`[conxa] Error: ${e.message}\n`);
390
+ process.exit(1);
391
+ }
392
+ }
393
+
394
+ if (require.main === module) {
395
+ runCli(process.argv.slice(2));
396
+ }
397
+
398
+ module.exports = { init, install, uninstall, list, search, registryLogin, registryLogout, runCli };
package/lib/config.js ADDED
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+
6
+ const CONXA_HOME = path.join(os.homedir(), ".conxa");
7
+ const REGISTRY_PATH = path.join(CONXA_HOME, "registry.json");
8
+ const CONXA_CLAUDE_MD = path.join(CONXA_HOME, "CLAUDE.md");
9
+ const CONXA_INDEX_MD = path.join(CONXA_HOME, "index.md");
10
+ const CACHE_DIR = path.join(CONXA_HOME, "cache");
11
+ const AUTH_DIR = path.join(CONXA_HOME, "auth");
12
+ const REGISTRY_AUTH_PATH = path.join(AUTH_DIR, "registry.json");
13
+
14
+ function getPluginDir(slug) {
15
+ return path.join(CONXA_HOME, "plugins", slug);
16
+ }
17
+
18
+ function getAuthJson(slug) {
19
+ return path.join(CONXA_HOME, "plugins", slug, "auth", "auth.json");
20
+ }
21
+
22
+ function getPluginConfig(slug) {
23
+ const cfgPath = path.join(getPluginDir(slug), "plugin.json");
24
+ return JSON.parse(fs.readFileSync(cfgPath, "utf8"));
25
+ }
26
+
27
+ function getRegistry() {
28
+ if (!fs.existsSync(REGISTRY_PATH)) return {};
29
+ try { return JSON.parse(fs.readFileSync(REGISTRY_PATH, "utf8")); } catch (_) { return {}; }
30
+ }
31
+
32
+ function getRegistryAuth() {
33
+ if (!fs.existsSync(REGISTRY_AUTH_PATH)) return { registries: [] };
34
+ try { return JSON.parse(fs.readFileSync(REGISTRY_AUTH_PATH, "utf8")); }
35
+ catch (_) { return { registries: [] }; }
36
+ }
37
+
38
+ function writeRegistryAuth(payload) {
39
+ fs.mkdirSync(AUTH_DIR, { recursive: true });
40
+ fs.writeFileSync(REGISTRY_AUTH_PATH, JSON.stringify(payload, null, 2) + "\n", { mode: 0o600 });
41
+ }
42
+
43
+ module.exports = {
44
+ CONXA_HOME, REGISTRY_PATH, CONXA_CLAUDE_MD, CONXA_INDEX_MD,
45
+ CACHE_DIR, AUTH_DIR, REGISTRY_AUTH_PATH,
46
+ getPluginDir, getAuthJson, getPluginConfig, getRegistry,
47
+ getRegistryAuth, writeRegistryAuth,
48
+ };
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "conxa-runtime",
3
+ "version": "1.0.0",
4
+ "description": "Conxa shared runtime — MCP server for all installed plugins",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js"
8
+ },
9
+ "dependencies": {
10
+ "@modelcontextprotocol/sdk": "^1.0.0",
11
+ "playwright": "^1.45.0"
12
+ },
13
+ "engines": {
14
+ "node": ">=20"
15
+ }
16
+ }