codexport 0.3.4 → 0.3.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.
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  the master serves a content-hashed bundle from its `~/.codex` directory. followers pin the master's fingerprint on join, fetch updates over a Tailscale-reachable HTTP address, and apply updates at Codex `SessionStart` through a short best-effort hook.
14
14
 
15
- MCPs are exported as full definitions, including env needed by supported tools. command-based MCPs are written through a managed launcher, so followers run `npx -y codexport@latest mcp run <name>` and let `codexport` translate master-local paths into repairable package, Python tool, or source-built launchers.
15
+ MCPs are exported as full definitions, including env needed by supported tools. command-based MCPs are written through a quiet local managed launcher, so followers run `node ~/.codexport/bin/codexport-mcp-run.mjs mcp run <name>` and let `codexport` translate master-local paths into repairable package, Python tool, or source-built launchers.
16
16
 
17
17
  [npm](https://www.npmjs.com/package/codexport) | [github](https://github.com/Microck/codexport)
18
18
 
@@ -123,8 +123,8 @@ all master MCP definitions are exported into `~/.codexport/mcp-manifest.json` on
123
123
 
124
124
  ```toml
125
125
  [mcp_servers.example]
126
- command = "npx"
127
- args = [ "-y", "codexport@latest", "mcp", "run", "example" ]
126
+ command = "node"
127
+ args = [ "~/.codexport/bin/codexport-mcp-run.mjs", "mcp", "run", "example" ]
128
128
  ```
129
129
 
130
130
  when Codex starts an MCP, `codexport mcp run` reads the original manifest entry, restores transferred environment values, rewrites master paths to follower paths, and chooses a runnable target:
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.3.4";
14
+ const VERSION = "0.3.6";
15
15
  const DEFAULT_PORT = 17342;
16
16
  const DEFAULT_TIMEOUT_MS = 5_000;
17
17
  const CODEXPORT_DIR = ".codexport";
@@ -22,7 +22,7 @@ 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
24
  const MCP_MANIFEST_FILE = "mcp-manifest.json";
25
- const MANAGED_MCP_PACKAGE = "codexport@latest";
25
+ const MANAGED_MCP_RUNNER_FILE = "codexport-mcp-run.mjs";
26
26
  const INCLUDE_ROOTS = [
27
27
  "AGENTS.md",
28
28
  "RTK.md",
@@ -465,9 +465,8 @@ function rewritePortableTableKeys(table, sourceRoot, sourceHome) {
465
465
  function rewriteManagedMcpServer(name, server, sourceRoot, sourceHome) {
466
466
  if (typeof server.url === "string")
467
467
  return;
468
- const args = Array.isArray(server.args) ? server.args : [];
469
- server.command = "npx";
470
- server.args = ["-y", MANAGED_MCP_PACKAGE, "mcp", "run", name];
468
+ server.command = "${node}";
469
+ server.args = ["${codexportMcpRunner}", "mcp", "run", name];
471
470
  if (server.env && typeof server.env === "object" && !Array.isArray(server.env)) {
472
471
  for (const [key, value] of Object.entries(server.env)) {
473
472
  if (typeof value === "string") {
@@ -499,7 +498,15 @@ function portableMcpLauncher(name, command, args, sourceHome, server) {
499
498
  }
500
499
  if (name === "discord-py-self" || commandName === "discord-py-self-mcp") {
501
500
  const remainingArgs = allStrings(args) ? args : [];
502
- return { command: "npx", args: ["-y", "discord-selfbot-mcp", ...remainingArgs] };
501
+ return {
502
+ command: "uvx",
503
+ args: ["--from", "git+https://github.com/Microck/discord.py-self-mcp.git", "discord-py-self-mcp", ...remainingArgs],
504
+ repair: {
505
+ whenMissing: "uvx",
506
+ command: "__codexport_install_uv",
507
+ args: []
508
+ }
509
+ };
503
510
  }
504
511
  if (name === "qmd" || commandName === "qmd") {
505
512
  const remainingArgs = allStrings(args) ? args : [];
@@ -752,6 +759,7 @@ async function applyBundle(ctx, bundle) {
752
759
  const localConfig = await readLocalConfig(ctx);
753
760
  await assertSkillConflicts(ctx, bundle, localConfig);
754
761
  await ensureDir(ctx.codexDir);
762
+ const managedMcpRunner = await writeManagedMcpRunner(ctx);
755
763
  const nextFiles = new Set(bundle.files.map((file) => file.path));
756
764
  const previousFiles = await readJsonIfExists(path.join(ctx.stateDir, APPLIED_FILES_FILE)) ?? [];
757
765
  for (const previousFile of previousFiles) {
@@ -777,7 +785,15 @@ async function applyBundle(ctx, bundle) {
777
785
  if (manifest) {
778
786
  await writeJsonAtomic(path.join(ctx.stateDir, MCP_MANIFEST_FILE), manifest);
779
787
  }
780
- const generated = mergeTomlText(canonicalConfig, localMcpText, { ...localConfig, codexDir: ctx.codexDir }, bundle.sourceRoot, bundle.sourceEnv);
788
+ const generated = mergeTomlText(canonicalConfig, localMcpText, {
789
+ ...localConfig,
790
+ codexDir: ctx.codexDir,
791
+ pathVariables: {
792
+ ...(localConfig.pathVariables ?? {}),
793
+ node: process.execPath,
794
+ codexportMcpRunner: managedMcpRunner
795
+ }
796
+ }, bundle.sourceRoot, bundle.sourceEnv);
781
797
  const configPath = path.join(ctx.codexDir, "config.toml");
782
798
  if (await pathExists(configPath)) {
783
799
  const backupPath = `${configPath}.codexport-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`;
@@ -794,6 +810,15 @@ async function applyBundle(ctx, bundle) {
794
810
  await writeLocalConfig(ctx, { ...localConfig, lastRevision: bundle.revision, codexDir: ctx.codexDir });
795
811
  await writeJsonAtomic(path.join(ctx.stateDir, APPLIED_FILES_FILE), bundle.files.map((file) => file.path));
796
812
  }
813
+ async function writeManagedMcpRunner(ctx) {
814
+ const binDir = path.join(ctx.stateDir, "bin");
815
+ const runnerPath = path.join(binDir, MANAGED_MCP_RUNNER_FILE);
816
+ await ensureDir(binDir);
817
+ await writeFileReplacingExisting(runnerPath, await readFile(fileURLToPath(import.meta.url)), { mode: 0o755 });
818
+ if (platform() !== "win32")
819
+ await chmod(runnerPath, 0o755);
820
+ return runnerPath;
821
+ }
797
822
  async function commandMcpRun(ctx, name) {
798
823
  const manifest = await readJsonIfExists(path.join(ctx.stateDir, MCP_MANIFEST_FILE));
799
824
  if (!manifest?.servers?.[name]) {
@@ -997,7 +1022,7 @@ async function installHook(ctx, timeoutMs) {
997
1022
  const existingText = await readTextIfExists(hooksPath);
998
1023
  const existing = existingText ? JSON.parse(existingText) : {};
999
1024
  const hooks = existing.SessionStart && Array.isArray(existing.SessionStart) ? existing.SessionStart : [];
1000
- const command = `codexport sync --apply --timeout-ms ${timeoutMs} --no-input`;
1025
+ const command = `codexport hook sync --timeout-ms ${timeoutMs} --no-input`;
1001
1026
  const filtered = hooks.filter((hook) => !(hook && typeof hook === "object" && hook.name === "codexport-sync"));
1002
1027
  filtered.push({ name: "codexport-sync", command, timeoutMs });
1003
1028
  await writeJsonAtomic(hooksPath, { ...existing, SessionStart: filtered });
@@ -1228,6 +1253,16 @@ async function commandSync(ctx, options) {
1228
1253
  print(ctx, { status: "staged", revision: bundle.revision, files: bundle.files.length });
1229
1254
  }
1230
1255
  }
1256
+ async function commandHookSync(ctx, options) {
1257
+ try {
1258
+ await commandSync(ctx, { apply: true, timeoutMs: options.timeoutMs });
1259
+ }
1260
+ catch (error) {
1261
+ if (!ctx.quiet) {
1262
+ process.stderr.write(`codexport hook sync skipped: ${asError(error).message}\n`);
1263
+ }
1264
+ }
1265
+ }
1231
1266
  async function commandApply(ctx) {
1232
1267
  const bundle = await readCachedBundle(ctx);
1233
1268
  await applyBundle(ctx, bundle);
@@ -1328,8 +1363,12 @@ async function main(argv) {
1328
1363
  .action(async (options, command) => {
1329
1364
  const ctx = contextFromCommand(command);
1330
1365
  await installHook(ctx, options.timeoutMs);
1331
- print(ctx, { installed: true, hook: "SessionStart", command: `codexport sync --apply --timeout-ms ${options.timeoutMs} --no-input` });
1366
+ print(ctx, { installed: true, hook: "SessionStart", command: `codexport hook sync --timeout-ms ${options.timeoutMs} --no-input` });
1332
1367
  });
1368
+ hook.command("sync")
1369
+ .description("Best-effort follower sync for Codex SessionStart hooks.")
1370
+ .option("--timeout-ms <ms>", "hook sync timeout", parsePositiveInt, 3_000)
1371
+ .action(async (options, command) => commandHookSync(contextFromCommand(command), options));
1333
1372
  program.command("status")
1334
1373
  .description("Show local enrollment state and remote revision reachability.")
1335
1374
  .option("--timeout-ms <ms>", "network timeout", parsePositiveInt, 1_500)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexport",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "sync a canonical Codex setup from one master machine to follower machines",
5
5
  "author": "Microck <contact@micr.dev>",
6
6
  "license": "MIT",