@the-meridian/cli 1.3.0 → 1.4.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @the-meridian/cli
2
2
 
3
+ ## 1.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 464f665: The bundled Meridian agent skill describes the pricing page's return URL with placeholders instead of a code template, and wording that directory scanners read as a credential is gone. No behaviour change.
8
+
9
+ ## 1.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 20c9059: New `meridian skills install`: writes the Meridian agent skill into the project for Claude Code (`.claude/skills/meridian`), Codex (`.agents/skills/meridian`) and Cursor (`.cursor/skills/meridian`), and adds the Meridian MCP server to each agent's project config, merging with what is there. `meridian login --json` now prints the device code and URL on stderr, so the login can be approved instead of waiting with nothing to show.
14
+
15
+ ### Patch Changes
16
+
17
+ - 04cb7ba: The stored CLI token is only sent to the API that issued it. When `MERIDIAN_API_URL` or `--api-url` points somewhere else (for example after a shell sourced an app's `.env`), commands stop with an error instead of sending the token there, and `meridian login` signs in to that API afresh. `meridian login` now always records the API it signed in to.
18
+ - 621147d: Raise the `zod` dependency floor to `^4.6.6`, the first release that fixes advisory GHSA-mrhx-c7xc-m3w5. No change to commands or output.
19
+
3
20
  ## 1.3.0
4
21
 
5
22
  ### Minor Changes
package/README.md CHANGED
@@ -48,6 +48,8 @@ Device-code login: prints a short user code, opens your browser to the verificat
48
48
  | `--no-browser` | Print the URL instead of opening a browser (also implied by `CI` or `MERIDIAN_NO_BROWSER`) |
49
49
  | `--hostname <name>` | Hostname reported to Meridian for this device |
50
50
 
51
+ With `--json`, the code and URL still go to stderr, so the login can be approved while stdout carries only the JSON result.
52
+
51
53
  ### `meridian logout`
52
54
 
53
55
  Deletes the stored token. Leaves `.meridian/project.json` alone, and exits `0` when you were not logged in.
@@ -108,6 +110,17 @@ meridian db credential revoke aws-backend --env production --yes
108
110
 
109
111
  Creating and rotating credentials happen in the dashboard (Hosting > Database > External access), behind a browser session: a CLI token cannot issue database access. Revoking needs the Credentials: Manage permission on the app.
110
112
 
113
+ ### `meridian skills install`
114
+
115
+ Installs the Meridian agent skill in the project, so Claude Code, Codex and Cursor know how to use Meridian: the CLI, the SDK, hosting and the Meridian MCP server. It writes the skill to `.claude/skills/meridian`, `.agents/skills/meridian` (Codex) and `.cursor/skills/meridian`, and adds the Meridian MCP server to `.mcp.json`, `.codex/config.toml` and `.cursor/mcp.json`. Existing config files are merged, never overwritten. Run it again after upgrading the CLI to update the skill.
116
+
117
+ | Flag | Behavior |
118
+ | --- | --- |
119
+ | `--agent <agents>` | `claude`, `codex`, `cursor` (comma-separated) or `all`. Default: the agents the project already uses (`.claude/`, `CLAUDE.md`, `.codex/`, `.agents/`, `AGENTS.md`, `.cursor/`), or all three |
120
+ | `--no-mcp` | Install the skill only |
121
+
122
+ Replacing a skill folder that differs from this CLI's version asks first (`--yes` to approve).
123
+
111
124
  ### Global flags
112
125
 
113
126
  Accepted before or after the command name, so both `meridian --json whoami` and `meridian whoami --json` work.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { realpathSync } from "fs";
5
- import { fileURLToPath } from "url";
5
+ import { fileURLToPath as fileURLToPath2 } from "url";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/db.ts
@@ -673,6 +673,16 @@ function resolveApiBaseUrl(override) {
673
673
  }
674
674
  return normalizeApiBaseUrl(DEFAULT_API_BASE_URL);
675
675
  }
676
+ function storedTokenApiBaseUrl(config = readConfig()) {
677
+ return normalizeApiBaseUrl(
678
+ config.api_base_url !== void 0 && config.api_base_url !== "" ? config.api_base_url : DEFAULT_API_BASE_URL
679
+ );
680
+ }
681
+ function tokenMismatchError(baseUrl, tokenBaseUrl) {
682
+ return new CliError(`Your Meridian CLI token was issued by ${tokenBaseUrl}, not ${baseUrl}, so it was not sent.`, {
683
+ hint: `Unset MERIDIAN_API_URL (or --api-url) to use ${tokenBaseUrl}, or run \`meridian login\` to sign in to ${baseUrl}.`
684
+ });
685
+ }
676
686
  function getAuth() {
677
687
  const config = readConfig();
678
688
  if (config.token === void 0) return null;
@@ -711,8 +721,13 @@ function workingDirectory() {
711
721
  function createApiClient(options = {}) {
712
722
  const requireAuth = options.requireAuth ?? true;
713
723
  const baseUrl = resolveApiBaseUrl(options.apiUrl ?? getGlobalOptions().apiUrl);
714
- const token = options.token ?? getAuth()?.token;
724
+ const storedToken = getAuth()?.token;
725
+ const token = options.token ?? storedToken;
715
726
  if (requireAuth && token === void 0) throw notLoggedInError();
727
+ if (token !== void 0 && token === storedToken) {
728
+ const tokenBaseUrl = storedTokenApiBaseUrl();
729
+ if (tokenBaseUrl !== baseUrl) throw tokenMismatchError(baseUrl, tokenBaseUrl);
730
+ }
716
731
  return new ApiClient({
717
732
  baseUrl,
718
733
  userAgent: USER_AGENT,
@@ -4053,7 +4068,8 @@ async function runLogin(options = {}) {
4053
4068
  const json = isJsonOutput();
4054
4069
  if (options.force !== true) {
4055
4070
  const auth = getAuth();
4056
- if (auth !== null) {
4071
+ const sameApi = auth !== null && storedTokenApiBaseUrl() === resolveApiBaseUrl(getGlobalOptions().apiUrl);
4072
+ if (auth !== null && sameApi) {
4057
4073
  const client2 = deps.createClient({ requireAuth: false, token: auth.token });
4058
4074
  try {
4059
4075
  const me = await client2.getMe();
@@ -4080,7 +4096,9 @@ async function runLogin(options = {}) {
4080
4096
  hostname,
4081
4097
  onCode: async (code) => {
4082
4098
  const target = code.verification_uri_complete ?? code.verification_uri;
4083
- if (!json) {
4099
+ if (json) {
4100
+ hint(`Enter the code ${formatUserCode(code.user_code)} at ${code.verification_uri} to approve this login.`);
4101
+ } else {
4084
4102
  log();
4085
4103
  log(` ${style.bold(formatUserCode(code.user_code))}`);
4086
4104
  log();
@@ -4106,12 +4124,11 @@ async function runLogin(options = {}) {
4106
4124
  });
4107
4125
  wait.stop();
4108
4126
  const user = toConfigUser(tokenResponse.user);
4109
- const storeApiBaseUrl = client.baseUrl !== DEFAULT_API_BASE_URL;
4110
4127
  updateConfig({
4111
4128
  token: tokenResponse.token,
4112
4129
  ...tokenResponse.expires_at !== void 0 ? { token_expires_at: tokenResponse.expires_at } : {},
4113
4130
  user,
4114
- ...storeApiBaseUrl ? { api_base_url: client.baseUrl } : {}
4131
+ api_base_url: client.baseUrl
4115
4132
  });
4116
4133
  if (json) {
4117
4134
  printLoginJson(user, tokenResponse.expires_at, client.baseUrl);
@@ -4586,6 +4603,208 @@ function registerMigrateCommand(program) {
4586
4603
  });
4587
4604
  }
4588
4605
 
4606
+ // src/commands/skills.ts
4607
+ import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
4608
+ import { dirname as dirname5, join as join8, relative as relative2 } from "path";
4609
+ import { fileURLToPath } from "url";
4610
+ var MCP_SERVER_URL = "https://mcp.the-meridian.ai/mcp";
4611
+ var SERVER_NAME = "meridian";
4612
+ var AGENT_IDS = ["claude", "codex", "cursor"];
4613
+ var AGENTS = {
4614
+ claude: {
4615
+ label: "Claude Code",
4616
+ skillDir: ".claude/skills/meridian",
4617
+ markers: [".claude", "CLAUDE.md", ".mcp.json"],
4618
+ mcpConfig: ".mcp.json",
4619
+ connectStep: "In Claude Code, run `/mcp` and approve Meridian in the browser."
4620
+ },
4621
+ codex: {
4622
+ label: "Codex",
4623
+ skillDir: ".agents/skills/meridian",
4624
+ markers: [".codex", ".agents", "AGENTS.md"],
4625
+ mcpConfig: ".codex/config.toml",
4626
+ connectStep: "Trust this project in Codex, then run `codex mcp login meridian`."
4627
+ },
4628
+ cursor: {
4629
+ label: "Cursor",
4630
+ skillDir: ".cursor/skills/meridian",
4631
+ markers: [".cursor", ".cursorrules"],
4632
+ mcpConfig: ".cursor/mcp.json",
4633
+ connectStep: "In Cursor, open Customize in the sidebar (Settings > Tools & MCP before Cursor 3) and connect Meridian."
4634
+ }
4635
+ };
4636
+ function bundledSkillDir() {
4637
+ const candidates = [
4638
+ new URL("./skill", import.meta.url),
4639
+ new URL("../../../skill/plugins/meridian/skills/meridian", import.meta.url)
4640
+ ].map((url) => fileURLToPath(url));
4641
+ const found = candidates.find((dir) => existsSync5(join8(dir, "SKILL.md")));
4642
+ if (found === void 0) {
4643
+ throw new CliError("This build of the Meridian CLI does not include the agent skill.", {
4644
+ hint: "Reinstall it with `npm install -g @the-meridian/cli@latest`."
4645
+ });
4646
+ }
4647
+ return found;
4648
+ }
4649
+ function parseAgents(raw) {
4650
+ const names = raw.split(",").map((name) => name.trim().toLowerCase()).filter((name) => name !== "");
4651
+ if (names.includes("all")) return [...AGENT_IDS];
4652
+ const unknown = names.filter((name) => !AGENT_IDS.includes(name));
4653
+ if (unknown.length > 0 || names.length === 0) {
4654
+ throw new CliError(`Unknown agent: ${unknown.join(", ") || raw}.`, {
4655
+ hint: `Use one or more of ${AGENT_IDS.join(", ")}, comma-separated, or \`all\`.`
4656
+ });
4657
+ }
4658
+ return [...new Set(names)];
4659
+ }
4660
+ function detectAgents(root) {
4661
+ const detected = AGENT_IDS.filter((id) => AGENTS[id].markers.some((marker) => existsSync5(join8(root, marker))));
4662
+ return detected.length > 0 ? detected : [...AGENT_IDS];
4663
+ }
4664
+ function listFiles(dir, base = dir) {
4665
+ return readdirSync2(dir, { withFileTypes: true }).flatMap((entry) => {
4666
+ const path = join8(dir, entry.name);
4667
+ return entry.isDirectory() ? listFiles(path, base) : [relative2(base, path)];
4668
+ }).sort();
4669
+ }
4670
+ function sameTree(a, b) {
4671
+ const filesA = listFiles(a);
4672
+ const filesB = listFiles(b);
4673
+ if (filesA.join("\n") !== filesB.join("\n")) return false;
4674
+ return filesA.every((file) => readFileSync7(join8(a, file)).equals(readFileSync7(join8(b, file))));
4675
+ }
4676
+ async function installSkill(root, agent, sourceDir, confirmImpl) {
4677
+ const target = join8(root, AGENTS[agent].skillDir);
4678
+ if (existsSync5(target)) {
4679
+ if (sameTree(sourceDir, target)) return "unchanged";
4680
+ const replace = await confirmAction({
4681
+ message: `Replace ${AGENTS[agent].skillDir} with this CLI's version? Changes made in that folder are lost.`,
4682
+ default: true,
4683
+ ...confirmImpl !== void 0 ? { promptImpl: confirmImpl } : {}
4684
+ });
4685
+ if (!replace) return "skipped";
4686
+ rmSync2(target, { recursive: true, force: true });
4687
+ cpSync(sourceDir, target, { recursive: true });
4688
+ return "updated";
4689
+ }
4690
+ mkdirSync4(dirname5(target), { recursive: true });
4691
+ cpSync(sourceDir, target, { recursive: true });
4692
+ return "created";
4693
+ }
4694
+ function readJsonObject(path) {
4695
+ if (!existsSync5(path)) return {};
4696
+ let parsed;
4697
+ try {
4698
+ parsed = JSON.parse(readFileSync7(path, "utf8"));
4699
+ } catch {
4700
+ throw new CliError(`${path} is not valid JSON, so the Meridian MCP server was not added to it.`, {
4701
+ hint: "Fix the file, or rerun with `--no-mcp` and add the server by hand."
4702
+ });
4703
+ }
4704
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4705
+ throw new CliError(`${path} does not hold a JSON object, so the Meridian MCP server was not added to it.`, {
4706
+ hint: "Fix the file, or rerun with `--no-mcp` and add the server by hand."
4707
+ });
4708
+ }
4709
+ return parsed;
4710
+ }
4711
+ function mergeJsonServer(path, server) {
4712
+ const config = readJsonObject(path);
4713
+ const existing = config.mcpServers;
4714
+ const servers = existing !== null && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
4715
+ const current2 = servers[SERVER_NAME];
4716
+ if (current2 !== void 0) {
4717
+ const url = current2?.url;
4718
+ return url === MCP_SERVER_URL ? "present" : "conflict";
4719
+ }
4720
+ config.mcpServers = { ...servers, [SERVER_NAME]: server };
4721
+ mkdirSync4(dirname5(path), { recursive: true });
4722
+ writeFileSync5(path, `${JSON.stringify(config, null, 2)}
4723
+ `);
4724
+ return "added";
4725
+ }
4726
+ function mergeTomlServer(path) {
4727
+ const current2 = existsSync5(path) ? readFileSync7(path, "utf8") : "";
4728
+ if (/^\s*\[mcp_servers\.meridian\]\s*$/m.test(current2)) {
4729
+ return current2.includes(`"${MCP_SERVER_URL}"`) ? "present" : "conflict";
4730
+ }
4731
+ const separator = current2 === "" || current2.endsWith("\n\n") ? "" : current2.endsWith("\n") ? "\n" : "\n\n";
4732
+ mkdirSync4(dirname5(path), { recursive: true });
4733
+ writeFileSync5(path, `${current2}${separator}[mcp_servers.meridian]
4734
+ url = "${MCP_SERVER_URL}"
4735
+ `);
4736
+ return "added";
4737
+ }
4738
+ function installMcp(root, agent) {
4739
+ const path = join8(root, AGENTS[agent].mcpConfig);
4740
+ switch (agent) {
4741
+ case "claude":
4742
+ return mergeJsonServer(path, { type: "http", url: MCP_SERVER_URL });
4743
+ case "cursor":
4744
+ return mergeJsonServer(path, { url: MCP_SERVER_URL });
4745
+ case "codex":
4746
+ return mergeTomlServer(path);
4747
+ }
4748
+ }
4749
+ var SKILL_MESSAGES = {
4750
+ created: "installed",
4751
+ updated: "updated",
4752
+ unchanged: "already up to date",
4753
+ skipped: "left as it was"
4754
+ };
4755
+ async function runSkillsInstall(options = {}) {
4756
+ const json = isJsonOutput();
4757
+ const root = readProject(workingDirectory())?.root ?? workingDirectory();
4758
+ const sourceDir = options.deps?.sourceDir ?? bundledSkillDir();
4759
+ const agents = options.agents ?? detectAgents(root);
4760
+ const result = { root, skills: [], mcp: [] };
4761
+ for (const agent of agents) {
4762
+ const status = await installSkill(root, agent, sourceDir, options.deps?.confirmImpl);
4763
+ result.skills.push({ agent, path: AGENTS[agent].skillDir, status });
4764
+ }
4765
+ if (options.mcp !== false) {
4766
+ for (const agent of agents) {
4767
+ result.mcp.push({ agent, path: AGENTS[agent].mcpConfig, status: installMcp(root, agent) });
4768
+ }
4769
+ }
4770
+ if (json) {
4771
+ printJson(result);
4772
+ return result;
4773
+ }
4774
+ for (const skill of result.skills) {
4775
+ success(`${AGENTS[skill.agent].label}: skill ${SKILL_MESSAGES[skill.status]} in ${skill.path}`);
4776
+ }
4777
+ for (const entry of result.mcp) {
4778
+ const label = AGENTS[entry.agent].label;
4779
+ if (entry.status === "conflict") {
4780
+ warn(`${label}: ${entry.path} already has a "${SERVER_NAME}" server with another URL. It was left alone.`);
4781
+ } else {
4782
+ success(`${label}: Meridian MCP server ${entry.status === "added" ? "added to" : "already in"} ${entry.path}`);
4783
+ }
4784
+ }
4785
+ const toConnect = result.mcp.filter((entry) => entry.status === "added");
4786
+ if (toConnect.length > 0) {
4787
+ log();
4788
+ for (const entry of toConnect) hint(AGENTS[entry.agent].connectStep);
4789
+ }
4790
+ hint("Commit these folders so your teammates' agents get the skill too.");
4791
+ return result;
4792
+ }
4793
+ function registerSkillsCommand(program) {
4794
+ const skills = program.command("skills").description("Install the Meridian agent skill for Claude Code, Codex and Cursor.");
4795
+ skills.action(() => {
4796
+ skills.outputHelp();
4797
+ });
4798
+ skills.command("install").description(
4799
+ "Write the Meridian skill into this project and add the Meridian MCP server to each agent's config. Run it again after upgrading the CLI to update the skill."
4800
+ ).option("--agent <agents>", `agents to install for: ${AGENT_IDS.join(", ")} or all (default: the ones this project uses)`).option("--no-mcp", "install the skill only, without the MCP server config").action(async (opts) => {
4801
+ await runSkillsInstall({
4802
+ ...opts.agent !== void 0 ? { agents: parseAgents(opts.agent) } : {},
4803
+ ...opts.mcp === false ? { mcp: false } : {}
4804
+ });
4805
+ });
4806
+ }
4807
+
4589
4808
  // src/commands/unlink.ts
4590
4809
  async function runUnlink(options = {}) {
4591
4810
  const deps = { ...options.deps };
@@ -4818,6 +5037,7 @@ function buildProgram() {
4818
5037
  registerDevCommand(program);
4819
5038
  registerMigrateCommand(program);
4820
5039
  registerDbCommand(program);
5040
+ registerSkillsCommand(program);
4821
5041
  addGlobalFlagsToSubcommands(program);
4822
5042
  return program;
4823
5043
  }
@@ -4833,7 +5053,7 @@ function isMainModule() {
4833
5053
  const entry = process.argv[1];
4834
5054
  if (entry === void 0) return false;
4835
5055
  try {
4836
- return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
5056
+ return realpathSync(entry) === realpathSync(fileURLToPath2(import.meta.url));
4837
5057
  } catch {
4838
5058
  return false;
4839
5059
  }