codexport 0.1.9 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +50 -13
  2. 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.1.9";
14
+ const VERSION = "0.2.0";
15
15
  const DEFAULT_PORT = 17342;
16
16
  const DEFAULT_TIMEOUT_MS = 5_000;
17
17
  const CODEXPORT_DIR = ".codexport";
@@ -50,6 +50,11 @@ const EXCLUDE_PARTS = new Set([
50
50
  ".sqlite",
51
51
  ".sqlite3"
52
52
  ]);
53
+ const MCP_ENV_EXPORT_NAMES = [
54
+ "KAGI_API_KEY",
55
+ "KAGI_SESSION_TOKEN",
56
+ "KAGI_CLI_PROFILE"
57
+ ];
53
58
  class CliError extends Error {
54
59
  exitCode;
55
60
  details;
@@ -232,26 +237,37 @@ async function walkIncluded(root, absolute, files) {
232
237
  });
233
238
  }
234
239
  }
235
- function computeRevision(files) {
240
+ function computeRevision(files, sourceEnv = {}) {
236
241
  const normalized = files.map((file) => ({
237
242
  path: file.path,
238
243
  mode: file.mode,
239
244
  kind: file.kind,
240
245
  contentHash: sha256(Buffer.from(file.content, "base64"))
241
246
  }));
242
- return sha256(JSON.stringify(normalized));
247
+ return sha256(JSON.stringify({ files: normalized, sourceEnv }));
243
248
  }
244
249
  async function buildBundle(codexDir) {
245
250
  const files = await collectFiles(codexDir);
246
- const revision = computeRevision(files);
251
+ const sourceEnv = collectSourceEnv();
252
+ const revision = computeRevision(files, sourceEnv);
247
253
  return {
248
254
  version: 1,
249
255
  builtAt: new Date().toISOString(),
250
256
  sourceRoot: codexDir,
251
257
  revision,
252
- files
258
+ files,
259
+ sourceEnv
253
260
  };
254
261
  }
262
+ function collectSourceEnv() {
263
+ const sourceEnv = {};
264
+ for (const name of MCP_ENV_EXPORT_NAMES) {
265
+ const value = process.env[name];
266
+ if (value)
267
+ sourceEnv[name] = value;
268
+ }
269
+ return sourceEnv;
270
+ }
255
271
  async function saveMasterBundle(ctx, bundle) {
256
272
  await writeJsonAtomic(path.join(ctx.stateDir, LAST_BUNDLE_FILE), bundle);
257
273
  }
@@ -327,7 +343,7 @@ function verifyBundle(bundle) {
327
343
  if (bundle.version !== 1 || !Array.isArray(bundle.files)) {
328
344
  throw new CliError("Bundle has an unsupported format.", 1);
329
345
  }
330
- const actualRevision = computeRevision(bundle.files);
346
+ const actualRevision = computeRevision(bundle.files, bundle.sourceEnv ?? {});
331
347
  if (bundle.revision !== actualRevision) {
332
348
  throw new CliError(`Bundle revision mismatch. Expected ${bundle.revision}, computed ${actualRevision}.`, 1);
333
349
  }
@@ -361,8 +377,8 @@ function extractTomlTableNames(text, prefix) {
361
377
  }
362
378
  return names;
363
379
  }
364
- function mergeTomlText(canonical, localMcpText, localConfig, sourceRoot) {
365
- const expandedCanonical = expandPathVariables(rewritePortableConfig(canonical, sourceRoot), localConfig);
380
+ function mergeTomlText(canonical, localMcpText, localConfig, sourceRoot, sourceEnv = {}) {
381
+ const expandedCanonical = expandPathVariables(rewritePortableConfig(canonical, sourceRoot, sourceEnv), localConfig);
366
382
  if (!localMcpText?.trim())
367
383
  return expandedCanonical;
368
384
  const canonicalMcps = extractTomlTableNames(canonical, "mcp_servers");
@@ -374,7 +390,7 @@ function mergeTomlText(canonical, localMcpText, localConfig, sourceRoot) {
374
390
  }
375
391
  return `${expandedCanonical.trimEnd()}\n\n# Follower-local MCP overlay from ~/.codexport/mcps.local.toml\n${localMcpText.trim()}\n`;
376
392
  }
377
- function rewritePortableConfig(canonical, sourceRoot) {
393
+ function rewritePortableConfig(canonical, sourceRoot, sourceEnv = {}) {
378
394
  let parsed;
379
395
  try {
380
396
  parsed = parseTomlObject(canonical, "canonical config.toml");
@@ -393,10 +409,21 @@ function rewritePortableConfig(canonical, sourceRoot) {
393
409
  if (!rawServer || typeof rawServer !== "object" || Array.isArray(rawServer))
394
410
  continue;
395
411
  const server = rawServer;
412
+ mergeSourceEnvForMcp(name, server, sourceEnv);
396
413
  rewritePortableMcpServer(name, server, sourceRoot, sourceHome);
397
414
  }
398
415
  return stringifyToml(parsed);
399
416
  }
417
+ function mergeSourceEnvForMcp(name, server, sourceEnv) {
418
+ if (name !== "kagi-mcp")
419
+ return;
420
+ const env = server.env && typeof server.env === "object" && !Array.isArray(server.env) ? server.env : {};
421
+ for (const key of ["KAGI_API_KEY", "KAGI_SESSION_TOKEN", "KAGI_CLI_PROFILE"]) {
422
+ if (typeof env[key] !== "string" && sourceEnv[key])
423
+ env[key] = sourceEnv[key];
424
+ }
425
+ server.env = env;
426
+ }
400
427
  function rewritePortableTableKeys(table, sourceRoot, sourceHome) {
401
428
  const rewritten = {};
402
429
  for (const [key, value] of Object.entries(table)) {
@@ -417,7 +444,7 @@ function rewritePortableMcpServer(_name, server, sourceRoot, sourceHome) {
417
444
  return;
418
445
  const command = typeof server.command === "string" ? server.command : undefined;
419
446
  const args = Array.isArray(server.args) ? server.args : [];
420
- const launcher = command && mcpHasRequiredPortableEnv(_name, command, server) ? portableMcpLauncher(_name, command, args, sourceHome) : undefined;
447
+ const launcher = command && mcpHasRequiredPortableEnv(_name, command, server) ? portableMcpLauncher(_name, command, args, sourceHome, server) : undefined;
421
448
  if (launcher) {
422
449
  server.command = launcher.command;
423
450
  server.args = launcher.args.map((arg) => rewritePortablePath(arg, sourceRoot, sourceHome));
@@ -442,11 +469,20 @@ function rewritePortableMcpServer(_name, server, sourceRoot, sourceHome) {
442
469
  ensurePortablePathEnv(server);
443
470
  }
444
471
  }
445
- function portableMcpLauncher(name, command, args, sourceHome) {
472
+ function portableMcpLauncher(name, command, args, sourceHome, server) {
446
473
  const commandName = basenameAnyPlatform(command);
447
474
  if (commandName === "npx" || commandName === "bunx" || commandName === "uvx") {
448
475
  return allStrings(args) ? { command: commandName, args: args } : undefined;
449
476
  }
477
+ if (name === "kagi-mcp" || commandName === "kagi-mcp") {
478
+ const env = server.env && typeof server.env === "object" && !Array.isArray(server.env) ? server.env : {};
479
+ if (typeof env.KAGI_API_KEY === "string" && env.KAGI_API_KEY.length > 0) {
480
+ return { command: "npx", args: ["-y", "kagi-mcp"] };
481
+ }
482
+ if (typeof env.KAGI_SESSION_TOKEN === "string" && env.KAGI_SESSION_TOKEN.length > 0) {
483
+ return { command: "npx", args: ["-y", "kagi-cli", "mcp"] };
484
+ }
485
+ }
450
486
  const nodePackage = nodePackageFromServer(command, args) ?? workspacePackageFromServer(command, args, sourceHome);
451
487
  if (nodePackage) {
452
488
  return { command: "npx", args: ["-y", nodePackage.packageName, ...nodePackage.remainingArgs] };
@@ -462,7 +498,8 @@ function mcpHasRequiredPortableEnv(name, command, server) {
462
498
  if (name !== "kagi-mcp" && basenameAnyPlatform(command) !== "kagi-mcp")
463
499
  return true;
464
500
  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;
501
+ return (typeof env?.KAGI_API_KEY === "string" && env.KAGI_API_KEY.length > 0)
502
+ || (typeof env?.KAGI_SESSION_TOKEN === "string" && env.KAGI_SESSION_TOKEN.length > 0);
466
503
  }
467
504
  function ensurePortablePathEnv(server) {
468
505
  const env = server.env && typeof server.env === "object" && !Array.isArray(server.env) ? server.env : {};
@@ -654,7 +691,7 @@ async function applyBundle(ctx, bundle) {
654
691
  if (configEntry) {
655
692
  const canonicalConfig = decodeFile(configEntry).toString("utf8");
656
693
  const localMcpText = await readTextIfExists(path.join(ctx.stateDir, MCPS_LOCAL_FILE));
657
- const generated = mergeTomlText(canonicalConfig, localMcpText, { ...localConfig, codexDir: ctx.codexDir }, bundle.sourceRoot);
694
+ const generated = mergeTomlText(canonicalConfig, localMcpText, { ...localConfig, codexDir: ctx.codexDir }, bundle.sourceRoot, bundle.sourceEnv);
658
695
  const configPath = path.join(ctx.codexDir, "config.toml");
659
696
  if (await pathExists(configPath)) {
660
697
  const backupPath = `${configPath}.codexport-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexport",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
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",