codexport 0.1.9 → 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 +138 -29
- 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",
|
|
@@ -50,6 +52,11 @@ const EXCLUDE_PARTS = new Set([
|
|
|
50
52
|
".sqlite",
|
|
51
53
|
".sqlite3"
|
|
52
54
|
]);
|
|
55
|
+
const MCP_ENV_EXPORT_NAMES = [
|
|
56
|
+
"KAGI_API_KEY",
|
|
57
|
+
"KAGI_SESSION_TOKEN",
|
|
58
|
+
"KAGI_CLI_PROFILE"
|
|
59
|
+
];
|
|
53
60
|
class CliError extends Error {
|
|
54
61
|
exitCode;
|
|
55
62
|
details;
|
|
@@ -232,26 +239,37 @@ async function walkIncluded(root, absolute, files) {
|
|
|
232
239
|
});
|
|
233
240
|
}
|
|
234
241
|
}
|
|
235
|
-
function computeRevision(files) {
|
|
242
|
+
function computeRevision(files, sourceEnv = {}) {
|
|
236
243
|
const normalized = files.map((file) => ({
|
|
237
244
|
path: file.path,
|
|
238
245
|
mode: file.mode,
|
|
239
246
|
kind: file.kind,
|
|
240
247
|
contentHash: sha256(Buffer.from(file.content, "base64"))
|
|
241
248
|
}));
|
|
242
|
-
return sha256(JSON.stringify(normalized));
|
|
249
|
+
return sha256(JSON.stringify({ files: normalized, sourceEnv }));
|
|
243
250
|
}
|
|
244
251
|
async function buildBundle(codexDir) {
|
|
245
252
|
const files = await collectFiles(codexDir);
|
|
246
|
-
const
|
|
253
|
+
const sourceEnv = collectSourceEnv();
|
|
254
|
+
const revision = computeRevision(files, sourceEnv);
|
|
247
255
|
return {
|
|
248
256
|
version: 1,
|
|
249
257
|
builtAt: new Date().toISOString(),
|
|
250
258
|
sourceRoot: codexDir,
|
|
251
259
|
revision,
|
|
252
|
-
files
|
|
260
|
+
files,
|
|
261
|
+
sourceEnv
|
|
253
262
|
};
|
|
254
263
|
}
|
|
264
|
+
function collectSourceEnv() {
|
|
265
|
+
const sourceEnv = {};
|
|
266
|
+
for (const name of MCP_ENV_EXPORT_NAMES) {
|
|
267
|
+
const value = process.env[name];
|
|
268
|
+
if (value)
|
|
269
|
+
sourceEnv[name] = value;
|
|
270
|
+
}
|
|
271
|
+
return sourceEnv;
|
|
272
|
+
}
|
|
255
273
|
async function saveMasterBundle(ctx, bundle) {
|
|
256
274
|
await writeJsonAtomic(path.join(ctx.stateDir, LAST_BUNDLE_FILE), bundle);
|
|
257
275
|
}
|
|
@@ -327,7 +345,7 @@ function verifyBundle(bundle) {
|
|
|
327
345
|
if (bundle.version !== 1 || !Array.isArray(bundle.files)) {
|
|
328
346
|
throw new CliError("Bundle has an unsupported format.", 1);
|
|
329
347
|
}
|
|
330
|
-
const actualRevision = computeRevision(bundle.files);
|
|
348
|
+
const actualRevision = computeRevision(bundle.files, bundle.sourceEnv ?? {});
|
|
331
349
|
if (bundle.revision !== actualRevision) {
|
|
332
350
|
throw new CliError(`Bundle revision mismatch. Expected ${bundle.revision}, computed ${actualRevision}.`, 1);
|
|
333
351
|
}
|
|
@@ -361,8 +379,8 @@ function extractTomlTableNames(text, prefix) {
|
|
|
361
379
|
}
|
|
362
380
|
return names;
|
|
363
381
|
}
|
|
364
|
-
function mergeTomlText(canonical, localMcpText, localConfig, sourceRoot) {
|
|
365
|
-
const expandedCanonical = expandPathVariables(rewritePortableConfig(canonical, sourceRoot), localConfig);
|
|
382
|
+
function mergeTomlText(canonical, localMcpText, localConfig, sourceRoot, sourceEnv = {}) {
|
|
383
|
+
const expandedCanonical = expandPathVariables(rewritePortableConfig(canonical, sourceRoot, sourceEnv), localConfig);
|
|
366
384
|
if (!localMcpText?.trim())
|
|
367
385
|
return expandedCanonical;
|
|
368
386
|
const canonicalMcps = extractTomlTableNames(canonical, "mcp_servers");
|
|
@@ -374,7 +392,7 @@ function mergeTomlText(canonical, localMcpText, localConfig, sourceRoot) {
|
|
|
374
392
|
}
|
|
375
393
|
return `${expandedCanonical.trimEnd()}\n\n# Follower-local MCP overlay from ~/.codexport/mcps.local.toml\n${localMcpText.trim()}\n`;
|
|
376
394
|
}
|
|
377
|
-
function rewritePortableConfig(canonical, sourceRoot) {
|
|
395
|
+
function rewritePortableConfig(canonical, sourceRoot, sourceEnv = {}) {
|
|
378
396
|
let parsed;
|
|
379
397
|
try {
|
|
380
398
|
parsed = parseTomlObject(canonical, "canonical config.toml");
|
|
@@ -393,10 +411,42 @@ function rewritePortableConfig(canonical, sourceRoot) {
|
|
|
393
411
|
if (!rawServer || typeof rawServer !== "object" || Array.isArray(rawServer))
|
|
394
412
|
continue;
|
|
395
413
|
const server = rawServer;
|
|
396
|
-
|
|
414
|
+
mergeSourceEnvForMcp(name, server, sourceEnv);
|
|
415
|
+
rewriteManagedMcpServer(name, server, sourceRoot, sourceHome);
|
|
397
416
|
}
|
|
398
417
|
return stringifyToml(parsed);
|
|
399
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
|
+
}
|
|
440
|
+
function mergeSourceEnvForMcp(name, server, sourceEnv) {
|
|
441
|
+
if (name !== "kagi-mcp")
|
|
442
|
+
return;
|
|
443
|
+
const env = server.env && typeof server.env === "object" && !Array.isArray(server.env) ? server.env : {};
|
|
444
|
+
for (const key of ["KAGI_API_KEY", "KAGI_SESSION_TOKEN", "KAGI_CLI_PROFILE"]) {
|
|
445
|
+
if (typeof env[key] !== "string" && sourceEnv[key])
|
|
446
|
+
env[key] = sourceEnv[key];
|
|
447
|
+
}
|
|
448
|
+
server.env = env;
|
|
449
|
+
}
|
|
400
450
|
function rewritePortableTableKeys(table, sourceRoot, sourceHome) {
|
|
401
451
|
const rewritten = {};
|
|
402
452
|
for (const [key, value] of Object.entries(table)) {
|
|
@@ -412,25 +462,12 @@ function rewritePortableTableKeys(table, sourceRoot, sourceHome) {
|
|
|
412
462
|
table[key] = value;
|
|
413
463
|
}
|
|
414
464
|
}
|
|
415
|
-
function
|
|
465
|
+
function rewriteManagedMcpServer(name, server, sourceRoot, sourceHome) {
|
|
416
466
|
if (typeof server.url === "string")
|
|
417
467
|
return;
|
|
418
|
-
const command = typeof server.command === "string" ? server.command : undefined;
|
|
419
468
|
const args = Array.isArray(server.args) ? server.args : [];
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
server.command = launcher.command;
|
|
423
|
-
server.args = launcher.args.map((arg) => rewritePortablePath(arg, sourceRoot, sourceHome));
|
|
424
|
-
}
|
|
425
|
-
else if (command && isAbsoluteAnyPlatform(command)) {
|
|
426
|
-
server.enabled = false;
|
|
427
|
-
}
|
|
428
|
-
else if (command) {
|
|
429
|
-
server.command = rewritePortableCommand(command, sourceRoot);
|
|
430
|
-
}
|
|
431
|
-
if (!launcher && args.length) {
|
|
432
|
-
server.args = args.map((arg) => typeof arg === "string" ? rewritePortablePath(arg, sourceRoot, sourceHome) : arg);
|
|
433
|
-
}
|
|
469
|
+
server.command = "npx";
|
|
470
|
+
server.args = ["-y", MANAGED_MCP_PACKAGE, "mcp", "run", name];
|
|
434
471
|
if (server.env && typeof server.env === "object" && !Array.isArray(server.env)) {
|
|
435
472
|
for (const [key, value] of Object.entries(server.env)) {
|
|
436
473
|
if (typeof value === "string") {
|
|
@@ -442,11 +479,20 @@ function rewritePortableMcpServer(_name, server, sourceRoot, sourceHome) {
|
|
|
442
479
|
ensurePortablePathEnv(server);
|
|
443
480
|
}
|
|
444
481
|
}
|
|
445
|
-
function portableMcpLauncher(name, command, args, sourceHome) {
|
|
482
|
+
function portableMcpLauncher(name, command, args, sourceHome, server) {
|
|
446
483
|
const commandName = basenameAnyPlatform(command);
|
|
447
484
|
if (commandName === "npx" || commandName === "bunx" || commandName === "uvx") {
|
|
448
485
|
return allStrings(args) ? { command: commandName, args: args } : undefined;
|
|
449
486
|
}
|
|
487
|
+
if (name === "kagi-mcp" || commandName === "kagi-mcp") {
|
|
488
|
+
const env = server.env && typeof server.env === "object" && !Array.isArray(server.env) ? server.env : {};
|
|
489
|
+
if (typeof env.KAGI_API_KEY === "string" && env.KAGI_API_KEY.length > 0) {
|
|
490
|
+
return { command: "npx", args: ["-y", "kagi-mcp"] };
|
|
491
|
+
}
|
|
492
|
+
if (typeof env.KAGI_SESSION_TOKEN === "string" && env.KAGI_SESSION_TOKEN.length > 0) {
|
|
493
|
+
return { command: "npx", args: ["-y", "kagi-cli", "mcp"] };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
450
496
|
const nodePackage = nodePackageFromServer(command, args) ?? workspacePackageFromServer(command, args, sourceHome);
|
|
451
497
|
if (nodePackage) {
|
|
452
498
|
return { command: "npx", args: ["-y", nodePackage.packageName, ...nodePackage.remainingArgs] };
|
|
@@ -462,7 +508,8 @@ function mcpHasRequiredPortableEnv(name, command, server) {
|
|
|
462
508
|
if (name !== "kagi-mcp" && basenameAnyPlatform(command) !== "kagi-mcp")
|
|
463
509
|
return true;
|
|
464
510
|
const env = server.env && typeof server.env === "object" && !Array.isArray(server.env) ? server.env : undefined;
|
|
465
|
-
return typeof env?.KAGI_API_KEY === "string" && env.KAGI_API_KEY.length > 0
|
|
511
|
+
return (typeof env?.KAGI_API_KEY === "string" && env.KAGI_API_KEY.length > 0)
|
|
512
|
+
|| (typeof env?.KAGI_SESSION_TOKEN === "string" && env.KAGI_SESSION_TOKEN.length > 0);
|
|
466
513
|
}
|
|
467
514
|
function ensurePortablePathEnv(server) {
|
|
468
515
|
const env = server.env && typeof server.env === "object" && !Array.isArray(server.env) ? server.env : {};
|
|
@@ -654,7 +701,11 @@ async function applyBundle(ctx, bundle) {
|
|
|
654
701
|
if (configEntry) {
|
|
655
702
|
const canonicalConfig = decodeFile(configEntry).toString("utf8");
|
|
656
703
|
const localMcpText = await readTextIfExists(path.join(ctx.stateDir, MCPS_LOCAL_FILE));
|
|
657
|
-
const
|
|
704
|
+
const manifest = buildMcpManifest(canonicalConfig, bundle.sourceRoot, bundle.sourceEnv);
|
|
705
|
+
if (manifest) {
|
|
706
|
+
await writeJsonAtomic(path.join(ctx.stateDir, MCP_MANIFEST_FILE), manifest);
|
|
707
|
+
}
|
|
708
|
+
const generated = mergeTomlText(canonicalConfig, localMcpText, { ...localConfig, codexDir: ctx.codexDir }, bundle.sourceRoot, bundle.sourceEnv);
|
|
658
709
|
const configPath = path.join(ctx.codexDir, "config.toml");
|
|
659
710
|
if (await pathExists(configPath)) {
|
|
660
711
|
const backupPath = `${configPath}.codexport-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
@@ -671,6 +722,43 @@ async function applyBundle(ctx, bundle) {
|
|
|
671
722
|
await writeLocalConfig(ctx, { ...localConfig, lastRevision: bundle.revision, codexDir: ctx.codexDir });
|
|
672
723
|
await writeJsonAtomic(path.join(ctx.stateDir, APPLIED_FILES_FILE), bundle.files.map((file) => file.path));
|
|
673
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
|
+
}
|
|
674
762
|
async function copyDirectory(source, target) {
|
|
675
763
|
await ensureDir(target);
|
|
676
764
|
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
@@ -712,6 +800,23 @@ function runCommand(command, args) {
|
|
|
712
800
|
});
|
|
713
801
|
});
|
|
714
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
|
+
}
|
|
715
820
|
async function installMasterService(ctx, port, dryRun) {
|
|
716
821
|
const command = `${process.execPath} ${realpathSync(fileURLToPath(import.meta.url))} master serve --port ${port}`;
|
|
717
822
|
if (platform() === "linux") {
|
|
@@ -979,6 +1084,10 @@ async function main(argv) {
|
|
|
979
1084
|
program.command("apply")
|
|
980
1085
|
.description("Apply the last staged bundle.")
|
|
981
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));
|
|
982
1091
|
const hook = program.command("hook").description("Manage follower Codex hooks.");
|
|
983
1092
|
hook.command("install")
|
|
984
1093
|
.description("Install a follower-only Codex SessionStart sync hook.")
|