codexport 0.2.0 → 0.3.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/dist/index.js +90 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { homedir, platform } from "node:os";
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
13
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
14
|
-
const VERSION = "0.
|
|
14
|
+
const VERSION = "0.3.0";
|
|
15
15
|
const DEFAULT_PORT = 17342;
|
|
16
16
|
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
17
17
|
const CODEXPORT_DIR = ".codexport";
|
|
@@ -21,6 +21,8 @@ const MCPS_LOCAL_FILE = "mcps.local.toml";
|
|
|
21
21
|
const LAST_BUNDLE_FILE = "last-bundle.json";
|
|
22
22
|
const CACHE_BUNDLE_FILE = "bundle.json";
|
|
23
23
|
const APPLIED_FILES_FILE = "applied-files.json";
|
|
24
|
+
const MCP_MANIFEST_FILE = "mcp-manifest.json";
|
|
25
|
+
const MANAGED_MCP_PACKAGE = "codexport@latest";
|
|
24
26
|
const INCLUDE_ROOTS = [
|
|
25
27
|
"AGENTS.md",
|
|
26
28
|
"RTK.md",
|
|
@@ -410,10 +412,31 @@ function rewritePortableConfig(canonical, sourceRoot, sourceEnv = {}) {
|
|
|
410
412
|
continue;
|
|
411
413
|
const server = rawServer;
|
|
412
414
|
mergeSourceEnvForMcp(name, server, sourceEnv);
|
|
413
|
-
|
|
415
|
+
rewriteManagedMcpServer(name, server, sourceRoot, sourceHome);
|
|
414
416
|
}
|
|
415
417
|
return stringifyToml(parsed);
|
|
416
418
|
}
|
|
419
|
+
function buildMcpManifest(canonical, sourceRoot, sourceEnv = {}) {
|
|
420
|
+
let parsed;
|
|
421
|
+
try {
|
|
422
|
+
parsed = parseTomlObject(canonical, "canonical config.toml");
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
const mcpServers = parsed.mcp_servers;
|
|
428
|
+
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers))
|
|
429
|
+
return undefined;
|
|
430
|
+
const servers = {};
|
|
431
|
+
for (const [name, rawServer] of Object.entries(mcpServers)) {
|
|
432
|
+
if (!rawServer || typeof rawServer !== "object" || Array.isArray(rawServer))
|
|
433
|
+
continue;
|
|
434
|
+
const server = structuredClone(rawServer);
|
|
435
|
+
mergeSourceEnvForMcp(name, server, sourceEnv);
|
|
436
|
+
servers[name] = server;
|
|
437
|
+
}
|
|
438
|
+
return { version: 1, sourceRoot, sourceEnv, servers };
|
|
439
|
+
}
|
|
417
440
|
function mergeSourceEnvForMcp(name, server, sourceEnv) {
|
|
418
441
|
if (name !== "kagi-mcp")
|
|
419
442
|
return;
|
|
@@ -439,25 +462,12 @@ function rewritePortableTableKeys(table, sourceRoot, sourceHome) {
|
|
|
439
462
|
table[key] = value;
|
|
440
463
|
}
|
|
441
464
|
}
|
|
442
|
-
function
|
|
465
|
+
function rewriteManagedMcpServer(name, server, sourceRoot, sourceHome) {
|
|
443
466
|
if (typeof server.url === "string")
|
|
444
467
|
return;
|
|
445
|
-
const command = typeof server.command === "string" ? server.command : undefined;
|
|
446
468
|
const args = Array.isArray(server.args) ? server.args : [];
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
server.command = launcher.command;
|
|
450
|
-
server.args = launcher.args.map((arg) => rewritePortablePath(arg, sourceRoot, sourceHome));
|
|
451
|
-
}
|
|
452
|
-
else if (command && isAbsoluteAnyPlatform(command)) {
|
|
453
|
-
server.enabled = false;
|
|
454
|
-
}
|
|
455
|
-
else if (command) {
|
|
456
|
-
server.command = rewritePortableCommand(command, sourceRoot);
|
|
457
|
-
}
|
|
458
|
-
if (!launcher && args.length) {
|
|
459
|
-
server.args = args.map((arg) => typeof arg === "string" ? rewritePortablePath(arg, sourceRoot, sourceHome) : arg);
|
|
460
|
-
}
|
|
469
|
+
server.command = "npx";
|
|
470
|
+
server.args = ["-y", MANAGED_MCP_PACKAGE, "mcp", "run", name];
|
|
461
471
|
if (server.env && typeof server.env === "object" && !Array.isArray(server.env)) {
|
|
462
472
|
for (const [key, value] of Object.entries(server.env)) {
|
|
463
473
|
if (typeof value === "string") {
|
|
@@ -691,6 +701,10 @@ async function applyBundle(ctx, bundle) {
|
|
|
691
701
|
if (configEntry) {
|
|
692
702
|
const canonicalConfig = decodeFile(configEntry).toString("utf8");
|
|
693
703
|
const localMcpText = await readTextIfExists(path.join(ctx.stateDir, MCPS_LOCAL_FILE));
|
|
704
|
+
const manifest = buildMcpManifest(canonicalConfig, bundle.sourceRoot, bundle.sourceEnv);
|
|
705
|
+
if (manifest) {
|
|
706
|
+
await writeJsonAtomic(path.join(ctx.stateDir, MCP_MANIFEST_FILE), manifest);
|
|
707
|
+
}
|
|
694
708
|
const generated = mergeTomlText(canonicalConfig, localMcpText, { ...localConfig, codexDir: ctx.codexDir }, bundle.sourceRoot, bundle.sourceEnv);
|
|
695
709
|
const configPath = path.join(ctx.codexDir, "config.toml");
|
|
696
710
|
if (await pathExists(configPath)) {
|
|
@@ -708,6 +722,43 @@ async function applyBundle(ctx, bundle) {
|
|
|
708
722
|
await writeLocalConfig(ctx, { ...localConfig, lastRevision: bundle.revision, codexDir: ctx.codexDir });
|
|
709
723
|
await writeJsonAtomic(path.join(ctx.stateDir, APPLIED_FILES_FILE), bundle.files.map((file) => file.path));
|
|
710
724
|
}
|
|
725
|
+
async function commandMcpRun(ctx, name) {
|
|
726
|
+
const manifest = await readJsonIfExists(path.join(ctx.stateDir, MCP_MANIFEST_FILE));
|
|
727
|
+
if (!manifest?.servers?.[name]) {
|
|
728
|
+
throw new CliError(`No managed MCP named ${name}. Run codexport sync --apply first.`, 1);
|
|
729
|
+
}
|
|
730
|
+
const localConfig = await readLocalConfig(ctx);
|
|
731
|
+
const sourceHome = inferHomeFromCodexDir(manifest.sourceRoot);
|
|
732
|
+
const server = structuredClone(manifest.servers[name]);
|
|
733
|
+
mergeSourceEnvForMcp(name, server, manifest.sourceEnv ?? {});
|
|
734
|
+
rewritePortableTableKeys(server, manifest.sourceRoot, sourceHome);
|
|
735
|
+
if (typeof server.url === "string") {
|
|
736
|
+
throw new CliError(`MCP ${name} is URL-based and should not be launched through codexport mcp run.`, 2);
|
|
737
|
+
}
|
|
738
|
+
const command = typeof server.command === "string" ? expandPathVariables(server.command, { ...localConfig, codexDir: ctx.codexDir }) : undefined;
|
|
739
|
+
const args = Array.isArray(server.args)
|
|
740
|
+
? server.args.map((arg) => typeof arg === "string" ? expandPathVariables(rewritePortablePath(arg, manifest.sourceRoot, sourceHome), { ...localConfig, codexDir: ctx.codexDir }) : String(arg))
|
|
741
|
+
: [];
|
|
742
|
+
if (!command)
|
|
743
|
+
throw new CliError(`MCP ${name} has no command.`, 1);
|
|
744
|
+
const launcher = mcpHasRequiredPortableEnv(name, command, server)
|
|
745
|
+
? portableMcpLauncher(name, command, args, sourceHome, server)
|
|
746
|
+
: undefined;
|
|
747
|
+
const runCommandName = launcher?.command ?? rewritePortableCommand(command, manifest.sourceRoot);
|
|
748
|
+
const runArgs = launcher?.args ?? args;
|
|
749
|
+
const childEnv = { ...process.env, ...portableServerEnv(server, manifest.sourceRoot, sourceHome, { ...localConfig, codexDir: ctx.codexDir }) };
|
|
750
|
+
await runCommandWithEnv(runCommandName, runArgs, childEnv);
|
|
751
|
+
}
|
|
752
|
+
function portableServerEnv(server, sourceRoot, sourceHome, localConfig) {
|
|
753
|
+
const env = server.env && typeof server.env === "object" && !Array.isArray(server.env) ? server.env : {};
|
|
754
|
+
const out = {};
|
|
755
|
+
for (const [key, value] of Object.entries(env)) {
|
|
756
|
+
if (typeof value === "string") {
|
|
757
|
+
out[key] = expandPathVariables(rewritePortablePath(value, sourceRoot, sourceHome), localConfig);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return out;
|
|
761
|
+
}
|
|
711
762
|
async function copyDirectory(source, target) {
|
|
712
763
|
await ensureDir(target);
|
|
713
764
|
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
@@ -749,6 +800,23 @@ function runCommand(command, args) {
|
|
|
749
800
|
});
|
|
750
801
|
});
|
|
751
802
|
}
|
|
803
|
+
function runCommandWithEnv(command, args, env) {
|
|
804
|
+
return new Promise((resolve, reject) => {
|
|
805
|
+
const child = spawn(command, args, { stdio: "inherit", env });
|
|
806
|
+
child.on("error", (error) => {
|
|
807
|
+
const message = error.code === "ENOENT"
|
|
808
|
+
? `MCP launcher program not found: ${command}. Install it on this follower or add it to PATH.`
|
|
809
|
+
: asError(error).message;
|
|
810
|
+
reject(new CliError(message, 1));
|
|
811
|
+
});
|
|
812
|
+
child.on("exit", (code) => {
|
|
813
|
+
if (code === 0)
|
|
814
|
+
resolve();
|
|
815
|
+
else
|
|
816
|
+
reject(new CliError(`${command} ${args.join(" ")} exited with ${code}`, code ?? 1));
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
}
|
|
752
820
|
async function installMasterService(ctx, port, dryRun) {
|
|
753
821
|
const command = `${process.execPath} ${realpathSync(fileURLToPath(import.meta.url))} master serve --port ${port}`;
|
|
754
822
|
if (platform() === "linux") {
|
|
@@ -1016,6 +1084,10 @@ async function main(argv) {
|
|
|
1016
1084
|
program.command("apply")
|
|
1017
1085
|
.description("Apply the last staged bundle.")
|
|
1018
1086
|
.action(async (_options, command) => commandApply(contextFromCommand(command)));
|
|
1087
|
+
const mcp = program.command("mcp").description("Run managed follower MCP launchers.");
|
|
1088
|
+
mcp.command("run <name>")
|
|
1089
|
+
.description("Run a synced MCP through codexport's managed launcher.")
|
|
1090
|
+
.action(async (name, _options, command) => commandMcpRun(contextFromCommand(command), name));
|
|
1019
1091
|
const hook = program.command("hook").description("Manage follower Codex hooks.");
|
|
1020
1092
|
hook.command("install")
|
|
1021
1093
|
.description("Install a follower-only Codex SessionStart sync hook.")
|