@elnora-ai/cli 1.2.1 → 1.3.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.
Files changed (2) hide show
  1. package/dist/cli.js +437 -204
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -398,7 +398,32 @@ function resolveApiKey(profileName) {
398
398
  migrateConfigIfNeeded();
399
399
  return getApiKey(profileName ?? "default");
400
400
  }
401
- var CONFIG_DIR, PROFILES_FILE, LEGACY_CONFIG_FILE, PROFILE_NAME_RE;
401
+ function truncateForDisplay(value) {
402
+ if (value.length > 20) return `${value.slice(0, 16)}...${value.slice(-4)}`;
403
+ return `${value.slice(0, 4)}...`;
404
+ }
405
+ function resolveCredentialHint(profileName) {
406
+ for (const varName of CREDENTIAL_ENV_VARS) {
407
+ const val2 = process.env[varName];
408
+ if (val2) return truncateForDisplay(val2);
409
+ }
410
+ migrateConfigIfNeeded();
411
+ const profiles = loadProfiles();
412
+ const name = profileName ?? "default";
413
+ if (!(name in profiles)) {
414
+ throw new AuthError(`Profile '${name}' not found.`, {
415
+ suggestion: "Run 'elnora auth login' to set up a profile."
416
+ });
417
+ }
418
+ const val = profiles[name][PROFILE_CREDENTIAL_FIELD] ?? "";
419
+ if (!val) {
420
+ throw new AuthError(`Profile '${name}' has no credential.`, {
421
+ suggestion: `Run: elnora auth login --profile ${name}`
422
+ });
423
+ }
424
+ return truncateForDisplay(val);
425
+ }
426
+ var CONFIG_DIR, PROFILES_FILE, LEGACY_CONFIG_FILE, PROFILE_NAME_RE, CREDENTIAL_ENV_VARS, PROFILE_CREDENTIAL_FIELD;
402
427
  var init_profiles = __esm({
403
428
  "src/lib/profiles.ts"() {
404
429
  "use strict";
@@ -407,6 +432,8 @@ var init_profiles = __esm({
407
432
  PROFILES_FILE = join(CONFIG_DIR, "profiles.toml");
408
433
  LEGACY_CONFIG_FILE = join(CONFIG_DIR, "config.toml");
409
434
  PROFILE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;
435
+ CREDENTIAL_ENV_VARS = ["ELNORA_API_KEY", "ELNORA_MCP_API_KEY"];
436
+ PROFILE_CREDENTIAL_FIELD = "api_key";
410
437
  }
411
438
  });
412
439
 
@@ -1906,7 +1933,7 @@ function generateZsh(root) {
1906
1933
  lines.push(" if (( CURRENT == 2 )); then");
1907
1934
  lines.push(" commands=(");
1908
1935
  for (const cmd of root.subcommands) {
1909
- lines.push(` '${cmd.name}:${cmd.description.replace(/'/g, "")}'`);
1936
+ lines.push(` '${cmd.name}:${cmd.description.replace(/\\/g, "\\\\").replace(/'/g, "")}'`);
1910
1937
  }
1911
1938
  lines.push(" )");
1912
1939
  lines.push(" _describe 'command' commands");
@@ -1920,7 +1947,7 @@ function generateZsh(root) {
1920
1947
  lines.push(" local -a subcommands");
1921
1948
  lines.push(" subcommands=(");
1922
1949
  for (const sub of group.subcommands) {
1923
- lines.push(` '${sub.name}:${sub.description.replace(/'/g, "")}'`);
1950
+ lines.push(` '${sub.name}:${sub.description.replace(/\\/g, "\\\\").replace(/'/g, "")}'`);
1924
1951
  }
1925
1952
  lines.push(" )");
1926
1953
  lines.push(" _describe 'subcommand' subcommands");
@@ -1941,14 +1968,14 @@ function generateFish(root) {
1941
1968
  ];
1942
1969
  for (const cmd of root.subcommands) {
1943
1970
  lines.push(
1944
- `complete -c elnora -n "__fish_use_subcommand" -a "${cmd.name}" -d "${cmd.description.replace(/"/g, '\\"')}"`
1971
+ `complete -c elnora -n "__fish_use_subcommand" -a "${cmd.name}" -d "${cmd.description.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
1945
1972
  );
1946
1973
  }
1947
1974
  lines.push("");
1948
1975
  for (const group of root.subcommands) {
1949
1976
  for (const sub of group.subcommands) {
1950
1977
  lines.push(
1951
- `complete -c elnora -n "__fish_seen_subcommand_from ${group.name}" -a "${sub.name}" -d "${sub.description.replace(/"/g, '\\"')}"`
1978
+ `complete -c elnora -n "__fish_seen_subcommand_from ${group.name}" -a "${sub.name}" -d "${sub.description.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
1952
1979
  );
1953
1980
  }
1954
1981
  }
@@ -1992,10 +2019,10 @@ var CACHE_DIR = join2(homedir2(), ".elnora");
1992
2019
  var CACHE_FILE = join2(CACHE_DIR, ".update-check");
1993
2020
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1994
2021
  var FETCH_TIMEOUT_MS = 3e3;
1995
- function shouldSkip() {
2022
+ var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/;
2023
+ function shouldSkipEntirely() {
1996
2024
  if (process.env.CI || process.env.GITHUB_ACTIONS) return true;
1997
2025
  if (process.env.ELNORA_NO_UPDATE_CHECK) return true;
1998
- if (!process.stderr.isTTY) return true;
1999
2026
  return false;
2000
2027
  }
2001
2028
  function readCache() {
@@ -2015,13 +2042,14 @@ function writeCache(entry) {
2015
2042
  }
2016
2043
  }
2017
2044
  function showUpdateNotice(latest) {
2018
- const color = isColorEnabled();
2019
- const msg = color ? `
2045
+ if (!process.stderr.isTTY) return;
2046
+ const color2 = isColorEnabled();
2047
+ const msg = color2 ? `
2020
2048
  ${pc.yellow(`Update available: v${VERSION} \u2192 v${latest}`)}
2021
- Run: curl -fsSL https://cli.elnora.ai/install.sh | bash
2049
+ Run: elnora update
2022
2050
  ` : `
2023
2051
  Update available: v${VERSION} \u2192 v${latest}
2024
- Run: curl -fsSL https://cli.elnora.ai/install.sh | bash
2052
+ Run: elnora update
2025
2053
  `;
2026
2054
  process.stderr.write(msg);
2027
2055
  }
@@ -2037,7 +2065,7 @@ function isNewerVersion(a, b) {
2037
2065
  return false;
2038
2066
  }
2039
2067
  function registerUpdateCheck() {
2040
- if (shouldSkip()) return;
2068
+ if (shouldSkipEntirely()) return;
2041
2069
  const cached = readCache();
2042
2070
  if (cached) {
2043
2071
  const elapsed = Date.now() - new Date(cached.checkedAt).getTime();
@@ -2050,7 +2078,9 @@ function registerUpdateCheck() {
2050
2078
  return;
2051
2079
  }
2052
2080
  }
2053
- setTimeout(async () => {
2081
+ const keepAlive = setInterval(() => {
2082
+ }, 6e4);
2083
+ (async () => {
2054
2084
  try {
2055
2085
  const response = await fetch(NPM_REGISTRY_URL, {
2056
2086
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
@@ -2058,22 +2088,25 @@ function registerUpdateCheck() {
2058
2088
  if (!response.ok) return;
2059
2089
  const data = await response.json();
2060
2090
  const latest = data.version ?? "unknown";
2091
+ if (latest !== "unknown" && !SEMVER_RE.test(latest)) return;
2061
2092
  writeCache({ checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latest });
2062
2093
  if (latest !== VERSION && latest !== "unknown" && isNewerVersion(latest, VERSION)) {
2063
2094
  showUpdateNotice(latest);
2064
2095
  }
2065
2096
  } catch {
2097
+ } finally {
2098
+ clearInterval(keepAlive);
2066
2099
  }
2067
- }, 0);
2100
+ })();
2068
2101
  }
2069
2102
 
2070
2103
  // src/commands/doctor.ts
2071
2104
  function addDoctorCommand(program2) {
2072
2105
  program2.command("doctor").description("Check Elnora CLI configuration and connectivity").action(async () => {
2073
- const color = isColorEnabled();
2074
- const ok = (msg) => color ? ` ${pc2.green("\u2713")} ${msg}` : ` \u2713 ${msg}`;
2075
- const fail = (msg) => color ? ` ${pc2.red("\u2717")} ${msg}` : ` \u2717 ${msg}`;
2076
- const warn = (msg) => color ? ` ${pc2.yellow("!")} ${msg}` : ` ! ${msg}`;
2106
+ const color2 = isColorEnabled();
2107
+ const ok2 = (msg) => color2 ? ` ${pc2.green("\u2713")} ${msg}` : ` \u2713 ${msg}`;
2108
+ const fail2 = (msg) => color2 ? ` ${pc2.red("\u2717")} ${msg}` : ` \u2717 ${msg}`;
2109
+ const warn = (msg) => color2 ? ` ${pc2.yellow("!")} ${msg}` : ` ! ${msg}`;
2077
2110
  console.error(`
2078
2111
  Elnora CLI v${VERSION}
2079
2112
  `);
@@ -2085,12 +2118,12 @@ Elnora CLI v${VERSION}
2085
2118
  const res = await fetch("https://platform.elnora.ai/health", { signal: AbortSignal.timeout(5e3) });
2086
2119
  const ms = Date.now() - start;
2087
2120
  if (res.ok) {
2088
- console.error(ok(`API reachable platform.elnora.ai (${res.status} OK, ${ms}ms)`));
2121
+ console.error(ok2(`API reachable platform.elnora.ai (${res.status} OK, ${ms}ms)`));
2089
2122
  passed++;
2090
- } else console.error(fail(`API unreachable platform.elnora.ai (HTTP ${res.status})`));
2123
+ } else console.error(fail2(`API unreachable platform.elnora.ai (HTTP ${res.status})`));
2091
2124
  } catch (e) {
2092
2125
  console.error(
2093
- fail(`API unreachable platform.elnora.ai (${e instanceof Error ? e.message : "network error"})`)
2126
+ fail2(`API unreachable platform.elnora.ai (${e instanceof Error ? e.message : "network error"})`)
2094
2127
  );
2095
2128
  }
2096
2129
  total++;
@@ -2101,10 +2134,10 @@ Elnora CLI v${VERSION}
2101
2134
  queryParams: { page: 1, pageSize: 1 }
2102
2135
  });
2103
2136
  const count = result?.totalCount ?? result?.items?.length ?? 0;
2104
- console.error(ok(`Authenticated profile: default (${count} projects accessible)`));
2137
+ console.error(ok2(`Authenticated profile: default (${count} projects accessible)`));
2105
2138
  passed++;
2106
2139
  } catch (e) {
2107
- console.error(fail(`Authentication failed ${e instanceof Error ? e.message : "no API key"}`));
2140
+ console.error(fail2(`Authentication failed ${e instanceof Error ? e.message : "no API key"}`));
2108
2141
  }
2109
2142
  total++;
2110
2143
  try {
@@ -2115,29 +2148,29 @@ Elnora CLI v${VERSION}
2115
2148
  const data = await res.json();
2116
2149
  const latest = data.version ?? "unknown";
2117
2150
  if (latest === VERSION || latest === "unknown" || !isNewerVersion(latest, VERSION)) {
2118
- console.error(ok(`Version current v${VERSION} (latest: v${latest})`));
2151
+ console.error(ok2(`Version current v${VERSION} (latest: v${latest})`));
2119
2152
  } else {
2120
2153
  console.error(warn(`Update available v${VERSION} \u2192 v${latest}`));
2121
2154
  }
2122
2155
  passed++;
2123
2156
  } else {
2124
- console.error(ok(`Version v${VERSION} (registry check failed)`));
2157
+ console.error(ok2(`Version v${VERSION} (registry check failed)`));
2125
2158
  passed++;
2126
2159
  }
2127
2160
  } catch {
2128
- console.error(ok(`Version v${VERSION} (registry unreachable)`));
2161
+ console.error(ok2(`Version v${VERSION} (registry unreachable)`));
2129
2162
  passed++;
2130
2163
  }
2131
2164
  total++;
2132
2165
  if (process.platform === "win32") {
2133
- console.error(ok("Config permissions (skipped on Windows)"));
2166
+ console.error(ok2("Config permissions (skipped on Windows)"));
2134
2167
  passed++;
2135
2168
  } else if (!existsSync3(PROFILES_FILE)) {
2136
2169
  console.error(warn("Config permissions ~/.elnora/profiles.toml not found"));
2137
2170
  } else {
2138
2171
  const mode = statSync(PROFILES_FILE).mode & 511;
2139
2172
  if (mode === 384) {
2140
- console.error(ok(`Config permissions ${PROFILES_FILE} (0600)`));
2173
+ console.error(ok2(`Config permissions ${PROFILES_FILE} (0600)`));
2141
2174
  passed++;
2142
2175
  } else {
2143
2176
  console.error(warn(`Config permissions ${PROFILES_FILE} (0${mode.toString(8)}) \u2014 should be 0600`));
@@ -2149,11 +2182,11 @@ Elnora CLI v${VERSION}
2149
2182
  const res = await fetch(`${AI_SERVER_URL}/health`, { signal: AbortSignal.timeout(5e3) });
2150
2183
  const ms = Date.now() - start;
2151
2184
  if (res.ok) {
2152
- console.error(ok(`AI Server reachable ${new URL(AI_SERVER_URL).hostname} (${res.status} OK, ${ms}ms)`));
2185
+ console.error(ok2(`AI Server reachable ${new URL(AI_SERVER_URL).hostname} (${res.status} OK, ${ms}ms)`));
2153
2186
  passed++;
2154
- } else console.error(fail(`AI Server unreachable ${new URL(AI_SERVER_URL).hostname} (HTTP ${res.status})`));
2187
+ } else console.error(fail2(`AI Server unreachable ${new URL(AI_SERVER_URL).hostname} (HTTP ${res.status})`));
2155
2188
  } catch {
2156
- console.error(fail(`AI Server unreachable ${AI_SERVER_URL}`));
2189
+ console.error(fail2(`AI Server unreachable ${AI_SERVER_URL}`));
2157
2190
  }
2158
2191
  console.error("");
2159
2192
  if (passed === total) {
@@ -4094,42 +4127,31 @@ function registerSearchCommands() {
4094
4127
  return [searchTasks, searchFiles, searchAll, searchFileContent];
4095
4128
  }
4096
4129
 
4097
- // src/commands/setup-claude.ts
4098
- init_config();
4099
- import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
4130
+ // src/commands/setup.ts
4131
+ init_profiles();
4132
+
4133
+ // src/lib/setup/claude.ts
4134
+ import { existsSync as existsSync5 } from "node:fs";
4135
+
4136
+ // src/lib/setup/common.ts
4137
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
4100
4138
  import { homedir as homedir3 } from "node:os";
4101
- import { dirname, join as join3 } from "node:path";
4102
- import { fileURLToPath } from "node:url";
4139
+ import { join as join3 } from "node:path";
4103
4140
  import pc3 from "picocolors";
4104
- var __script_dir;
4105
- try {
4106
- __script_dir = dirname(fileURLToPath(import.meta.url));
4107
- } catch {
4108
- __script_dir = typeof __dirname !== "undefined" ? __dirname : process.cwd();
4109
- }
4110
- var ELNORA_DIR = join3(homedir3(), ".elnora");
4111
- var PLUGIN_DIR = join3(ELNORA_DIR, "plugin", "elnora");
4112
- var CLAUDE_DIR = join3(homedir3(), ".claude");
4113
- var MARKETPLACES_FILE = join3(CLAUDE_DIR, "plugins", "known_marketplaces.json");
4114
- var SETTINGS_FILE = join3(CLAUDE_DIR, "settings.json");
4115
- var MARKETPLACE_NAME = "elnora-local";
4141
+ var HOME = homedir3();
4142
+ var ELNORA_DIR = join3(HOME, ".elnora");
4143
+ var CLAUDE_DIR = join3(HOME, ".claude");
4144
+ var CLAUDE_MARKETPLACES_FILE = join3(CLAUDE_DIR, "plugins", "known_marketplaces.json");
4145
+ var CLAUDE_SETTINGS_FILE = join3(CLAUDE_DIR, "settings.json");
4146
+ var CURSOR_DIR = join3(HOME, ".cursor");
4147
+ var CURSOR_MCP_GLOBAL = join3(HOME, ".cursor", "mcp.json");
4148
+ var VSCODE_DIR = join3(HOME, ".vscode");
4149
+ var CODEX_DIR = join3(HOME, ".codex");
4150
+ var MARKETPLACE_NAME = "elnora-ai";
4151
+ var MARKETPLACE_REPO = "Elnora-AI/elnora-plugins";
4116
4152
  var PLUGIN_ID = `elnora@${MARKETPLACE_NAME}`;
4117
- function findSkillsDir() {
4118
- const candidates = [
4119
- join3(__script_dir, "../../skills"),
4120
- join3(__script_dir, "../skills"),
4121
- // When running from source via tsx
4122
- join3(__script_dir, "../../../skills"),
4123
- // Standalone binary: skills extracted to ~/.elnora/skills/
4124
- join3(ELNORA_DIR, "skills")
4125
- ];
4126
- for (const dir of candidates) {
4127
- if (existsSync4(join3(dir, "elnora-platform", "SKILL.md"))) {
4128
- return dir;
4129
- }
4130
- }
4131
- return null;
4132
- }
4153
+ var LEGACY_PLUGIN_IDS = ["elnora@elnora-local"];
4154
+ var LEGACY_MARKETPLACE_NAMES = ["elnora-local"];
4133
4155
  function readJsonFile(path) {
4134
4156
  try {
4135
4157
  return JSON.parse(readFileSync5(path, "utf-8"));
@@ -4142,63 +4164,225 @@ function writeJsonFile(path, data) {
4142
4164
  writeFileSync3(path, `${JSON.stringify(data, null, 2)}
4143
4165
  `, "utf-8");
4144
4166
  }
4145
- function addSetupClaudeCommand(program2) {
4146
- program2.command("setup-claude").description("Install Elnora skills for Claude Code").action(() => {
4147
- const color = isColorEnabled();
4148
- const ok = (msg) => color ? ` ${pc3.green("\u2713")} ${msg}` : ` \u2713 ${msg}`;
4149
- const fail = (msg) => color ? ` ${pc3.red("\u2717")} ${msg}` : ` \u2717 ${msg}`;
4167
+ var color = isColorEnabled();
4168
+ function ok(msg) {
4169
+ return color ? ` ${pc3.green("\u2713")} ${msg}` : ` \u2713 ${msg}`;
4170
+ }
4171
+ function fail(msg) {
4172
+ return color ? ` ${pc3.red("\u2717")} ${msg}` : ` \u2717 ${msg}`;
4173
+ }
4174
+ function info(msg) {
4175
+ return color ? ` ${pc3.dim(msg)}` : ` ${msg}`;
4176
+ }
4177
+ function detectPlatforms() {
4178
+ return [
4179
+ { name: "claude", label: "Claude Code", installed: existsSync4(CLAUDE_DIR) },
4180
+ { name: "cursor", label: "Cursor", installed: existsSync4(CURSOR_DIR) },
4181
+ { name: "vscode", label: "VS Code", installed: existsSync4(VSCODE_DIR) },
4182
+ { name: "codex", label: "OpenAI Codex", installed: existsSync4(CODEX_DIR) }
4183
+ ];
4184
+ }
4185
+
4186
+ // src/lib/setup/claude.ts
4187
+ function setupClaude() {
4188
+ if (!existsSync5(CLAUDE_DIR)) {
4189
+ console.error(fail("Claude Code not found (~/.claude/ does not exist)"));
4150
4190
  console.error("");
4151
- if (!existsSync4(CLAUDE_DIR)) {
4152
- console.error(fail("Claude Code not found (~/.claude/ does not exist)"));
4153
- console.error("");
4154
- console.error(" Install Claude Code first: https://claude.ai/code");
4155
- process.exit(1);
4191
+ console.error(" Install Claude Code first: https://claude.ai/code");
4192
+ return false;
4193
+ }
4194
+ console.error(ok("Claude Code found"));
4195
+ const marketplaces = readJsonFile(CLAUDE_MARKETPLACES_FILE);
4196
+ for (const old of LEGACY_MARKETPLACE_NAMES) {
4197
+ delete marketplaces[old];
4198
+ }
4199
+ marketplaces[MARKETPLACE_NAME] = {
4200
+ source: {
4201
+ source: "github",
4202
+ repo: MARKETPLACE_REPO
4156
4203
  }
4157
- console.error(ok("Claude Code found"));
4158
- const skillsDir = findSkillsDir();
4159
- if (!skillsDir) {
4160
- console.error(fail("Skills not found"));
4161
- console.error("");
4162
- console.error(" Skills may not be bundled with your install method.");
4163
- console.error(" Try: npm install -g @elnora-ai/cli");
4164
- process.exit(1);
4204
+ };
4205
+ writeJsonFile(CLAUDE_MARKETPLACES_FILE, marketplaces);
4206
+ console.error(ok(`Marketplace registered (GitHub: ${MARKETPLACE_REPO})`));
4207
+ const settings = readJsonFile(CLAUDE_SETTINGS_FILE);
4208
+ if (!settings.enabledPlugins || typeof settings.enabledPlugins !== "object") {
4209
+ settings.enabledPlugins = {};
4210
+ }
4211
+ const plugins = settings.enabledPlugins;
4212
+ for (const old of LEGACY_PLUGIN_IDS) {
4213
+ delete plugins[old];
4214
+ }
4215
+ plugins[PLUGIN_ID] = true;
4216
+ writeJsonFile(CLAUDE_SETTINGS_FILE, settings);
4217
+ console.error(ok(`Plugin enabled (${PLUGIN_ID})`));
4218
+ console.error("");
4219
+ console.error(" Elnora skills are now available in Claude Code.");
4220
+ console.error(" Restart Claude Code if it's currently running.");
4221
+ return true;
4222
+ }
4223
+
4224
+ // src/lib/setup/codex.ts
4225
+ import { existsSync as existsSync6 } from "node:fs";
4226
+ import { join as join4 } from "node:path";
4227
+ var CODEX_MCP_FILE = join4(CODEX_DIR, "mcp.json");
4228
+ function setupCodex(apiKey) {
4229
+ if (!existsSync6(CODEX_DIR)) {
4230
+ console.error(fail("Codex CLI not found (~/.codex/ does not exist)"));
4231
+ console.error("");
4232
+ console.error(" Install Codex first: https://github.com/openai/codex");
4233
+ return false;
4234
+ }
4235
+ console.error(ok("Codex CLI found"));
4236
+ const config = readJsonFile(CODEX_MCP_FILE);
4237
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
4238
+ config.mcpServers = {};
4239
+ }
4240
+ config.mcpServers.elnora = {
4241
+ url: "https://mcp.elnora.ai/mcp",
4242
+ headers: {
4243
+ "X-API-Key": apiKey
4165
4244
  }
4166
- console.error(ok(`Skills found at ${skillsDir}`));
4167
- mkdirSync3(PLUGIN_DIR, { recursive: true });
4168
- writeJsonFile(join3(PLUGIN_DIR, ".claude-plugin", "plugin.json"), {
4169
- name: "elnora",
4170
- version: VERSION,
4171
- description: "AI-powered bioprotocol generation and lab workflow management. Connect to the Elnora AI Platform to generate, optimize, and manage bioprotocols.",
4172
- author: { name: "Elnora AI", url: "https://elnora.ai" },
4173
- repository: "https://github.com/Elnora-AI/elnora-cli",
4174
- homepage: "https://elnora.ai",
4175
- license: "Apache-2.0",
4176
- keywords: ["bioprotocol", "lab", "pharma", "life-sciences"]
4177
- });
4178
- cpSync(skillsDir, join3(PLUGIN_DIR, "skills"), { recursive: true });
4179
- console.error(ok("Skills installed to ~/.elnora/plugin/"));
4180
- const marketplaces = readJsonFile(MARKETPLACES_FILE);
4181
- marketplaces[MARKETPLACE_NAME] = {
4182
- source: {
4183
- source: "directory",
4184
- path: join3(ELNORA_DIR, "plugin")
4185
- },
4186
- installLocation: join3(ELNORA_DIR, "plugin"),
4187
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
4188
- };
4189
- writeJsonFile(MARKETPLACES_FILE, marketplaces);
4190
- console.error(ok("Marketplace registered in Claude Code"));
4191
- const settings = readJsonFile(SETTINGS_FILE);
4192
- if (!settings.enabledPlugins || typeof settings.enabledPlugins !== "object") {
4193
- settings.enabledPlugins = {};
4245
+ };
4246
+ writeJsonFile(CODEX_MCP_FILE, config);
4247
+ console.error(ok("MCP server configured in ~/.codex/mcp.json"));
4248
+ console.error("");
4249
+ console.error(" Elnora MCP server is now available in Codex.");
4250
+ return true;
4251
+ }
4252
+
4253
+ // src/lib/setup/cursor.ts
4254
+ import { existsSync as existsSync7 } from "node:fs";
4255
+ function setupCursor(apiKey) {
4256
+ if (!existsSync7(CURSOR_DIR)) {
4257
+ console.error(fail("Cursor not found (~/.cursor/ does not exist)"));
4258
+ console.error("");
4259
+ console.error(" Install Cursor first: https://cursor.com");
4260
+ return false;
4261
+ }
4262
+ console.error(ok("Cursor found"));
4263
+ const config = readJsonFile(CURSOR_MCP_GLOBAL);
4264
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
4265
+ config.mcpServers = {};
4266
+ }
4267
+ config.mcpServers.elnora = {
4268
+ url: "https://mcp.elnora.ai/mcp",
4269
+ headers: {
4270
+ "X-API-Key": apiKey
4194
4271
  }
4195
- settings.enabledPlugins[PLUGIN_ID] = true;
4196
- writeJsonFile(SETTINGS_FILE, settings);
4197
- console.error(ok("Plugin enabled"));
4272
+ };
4273
+ writeJsonFile(CURSOR_MCP_GLOBAL, config);
4274
+ console.error(ok("MCP server configured in ~/.cursor/mcp.json"));
4275
+ console.error("");
4276
+ console.error(" Elnora MCP server is now available in Cursor.");
4277
+ console.error(" Restart Cursor if it's currently running.");
4278
+ return true;
4279
+ }
4280
+
4281
+ // src/lib/setup/vscode.ts
4282
+ import { existsSync as existsSync8 } from "node:fs";
4283
+ import { join as join5 } from "node:path";
4284
+ var VSCODE_MCP_GLOBAL = join5(HOME, ".vscode", "mcp.json");
4285
+ function setupVscode(apiKey) {
4286
+ if (!existsSync8(VSCODE_DIR)) {
4287
+ console.error(fail("VS Code not found (~/.vscode/ does not exist)"));
4198
4288
  console.error("");
4199
- console.error(" Elnora skills are now available in Claude Code.");
4200
- console.error(" Restart Claude Code if it's currently running.");
4289
+ console.error(" Install VS Code first: https://code.visualstudio.com");
4290
+ return false;
4291
+ }
4292
+ console.error(ok("VS Code found"));
4293
+ const config = readJsonFile(VSCODE_MCP_GLOBAL);
4294
+ if (!config.servers || typeof config.servers !== "object") {
4295
+ config.servers = {};
4296
+ }
4297
+ config.servers.elnora = {
4298
+ type: "http",
4299
+ url: "https://mcp.elnora.ai/mcp",
4300
+ headers: {
4301
+ "X-API-Key": apiKey
4302
+ }
4303
+ };
4304
+ writeJsonFile(VSCODE_MCP_GLOBAL, config);
4305
+ console.error(ok("MCP server configured in ~/.vscode/mcp.json"));
4306
+ console.error("");
4307
+ console.error(" Elnora MCP server is now available in VS Code Copilot.");
4308
+ console.error(" Restart VS Code if it's currently running.");
4309
+ return true;
4310
+ }
4311
+
4312
+ // src/commands/setup.ts
4313
+ var PLATFORM_SETUP = {
4314
+ claude: () => setupClaude(),
4315
+ cursor: (key) => setupCursor(key),
4316
+ vscode: (key) => setupVscode(key),
4317
+ codex: (key) => setupCodex(key)
4318
+ };
4319
+ function runSetup(platforms, profileName) {
4320
+ let apiKey;
4321
+ const needsKey = platforms.some((p) => p !== "claude");
4322
+ if (needsKey) {
4323
+ try {
4324
+ apiKey = resolveApiKey(profileName);
4325
+ } catch {
4326
+ if (platforms.every((p) => p !== "claude")) {
4327
+ console.error(fail("No API key found. Run 'elnora auth login' first."));
4328
+ process.exitCode = 1;
4329
+ return;
4330
+ }
4331
+ }
4332
+ }
4333
+ let anyFailed = false;
4334
+ for (const platform of platforms) {
4335
+ const setupFn = PLATFORM_SETUP[platform];
4336
+ const success = setupFn(apiKey ?? "");
4337
+ if (!success) anyFailed = true;
4338
+ console.error("");
4339
+ }
4340
+ if (anyFailed) {
4341
+ process.exitCode = 1;
4342
+ }
4343
+ }
4344
+ function addSetupCommand(program2) {
4345
+ const setup = program2.command("setup").description("Configure AI coding tools to use Elnora").option("--profile <name>", "API key profile to use", "default");
4346
+ setup.command("claude").description("Set up Claude Code (skills + MCP server)").action(() => {
4347
+ const opts = setup.opts();
4201
4348
  console.error("");
4349
+ runSetup(["claude"], opts.profile);
4350
+ });
4351
+ setup.command("cursor").description("Set up Cursor (MCP server)").action(() => {
4352
+ const opts = setup.opts();
4353
+ console.error("");
4354
+ runSetup(["cursor"], opts.profile);
4355
+ });
4356
+ setup.command("vscode").description("Set up VS Code Copilot (MCP server)").action(() => {
4357
+ const opts = setup.opts();
4358
+ console.error("");
4359
+ runSetup(["vscode"], opts.profile);
4360
+ });
4361
+ setup.command("codex").description("Set up OpenAI Codex (MCP server)").action(() => {
4362
+ const opts = setup.opts();
4363
+ console.error("");
4364
+ runSetup(["codex"], opts.profile);
4365
+ });
4366
+ setup.action(() => {
4367
+ const opts = setup.opts();
4368
+ console.error("");
4369
+ const detected = detectPlatforms();
4370
+ const installed = detected.filter((p) => p.installed);
4371
+ if (installed.length === 0) {
4372
+ console.error(fail("No supported AI coding tools detected."));
4373
+ console.error("");
4374
+ console.error(" Supported: Claude Code, Cursor, VS Code, OpenAI Codex");
4375
+ console.error(" Install one first, then run 'elnora setup' again.");
4376
+ process.exitCode = 1;
4377
+ return;
4378
+ }
4379
+ console.error(ok(`Detected ${installed.length} platform(s):`));
4380
+ for (const p of installed) {
4381
+ console.error(info(`${p.label}`));
4382
+ }
4383
+ console.error("");
4384
+ const platforms = installed.map((p) => p.name);
4385
+ runSetup(platforms, opts.profile);
4202
4386
  });
4203
4387
  }
4204
4388
 
@@ -4689,10 +4873,34 @@ import { execFileSync } from "node:child_process";
4689
4873
  import { createHash } from "node:crypto";
4690
4874
  import { chmodSync as chmodSync2, copyFileSync, createWriteStream, mkdirSync as mkdirSync4, readFileSync as readFileSync6, rmSync } from "node:fs";
4691
4875
  import { tmpdir } from "node:os";
4692
- import { join as join4 } from "node:path";
4876
+ import { join as join6 } from "node:path";
4693
4877
  import { Readable } from "node:stream";
4694
4878
  import { pipeline } from "node:stream/promises";
4695
4879
  import pc5 from "picocolors";
4880
+
4881
+ // src/lib/install-method.ts
4882
+ function detectInstallMethod() {
4883
+ const execPath = process.argv[1] ?? "";
4884
+ if (execPath.includes("node_modules")) {
4885
+ return "npm";
4886
+ }
4887
+ if (execPath.includes("/Cellar/") || execPath.includes("/homebrew/")) {
4888
+ return "homebrew";
4889
+ }
4890
+ return "binary";
4891
+ }
4892
+ function getUpdateInstruction(method) {
4893
+ switch (method) {
4894
+ case "npm":
4895
+ return "npm update -g @elnora-ai/cli";
4896
+ case "homebrew":
4897
+ return "brew upgrade elnora";
4898
+ case "binary":
4899
+ return "elnora update --install";
4900
+ }
4901
+ }
4902
+
4903
+ // src/commands/update.ts
4696
4904
  var NPM_REGISTRY_URL2 = "https://registry.npmjs.org/@elnora-ai/cli/latest";
4697
4905
  var GITHUB_REPO = "Elnora-AI/elnora-cli";
4698
4906
  function getPlatformTarget() {
@@ -4708,8 +4916,11 @@ function getPlatformTarget() {
4708
4916
  return null;
4709
4917
  }
4710
4918
  }
4711
- function getManualHint() {
4712
- return process.platform === "win32" ? " irm https://cli.elnora.ai/install.ps1 | iex" : " curl -fsSL https://cli.elnora.ai/install.sh | bash";
4919
+ async function fetchLatestVersion() {
4920
+ const res = await fetch(NPM_REGISTRY_URL2, { signal: AbortSignal.timeout(5e3) });
4921
+ if (!res.ok) throw new Error(`Registry returned HTTP ${res.status}`);
4922
+ const data = await res.json();
4923
+ return data.version ?? "unknown";
4713
4924
  }
4714
4925
  async function downloadToFile(url, dest) {
4715
4926
  const res = await fetch(url, { signal: AbortSignal.timeout(12e4), redirect: "follow" });
@@ -4717,98 +4928,121 @@ async function downloadToFile(url, dest) {
4717
4928
  const nodeStream = Readable.fromWeb(res.body);
4718
4929
  await pipeline(nodeStream, createWriteStream(dest));
4719
4930
  }
4931
+ async function installBinaryUpdate(latest) {
4932
+ const color2 = isColorEnabled();
4933
+ const platformInfo = getPlatformTarget();
4934
+ if (!platformInfo) {
4935
+ console.error(`Unsupported platform: ${process.platform}.`);
4936
+ console.error("Try: npm update -g @elnora-ai/cli");
4937
+ process.exitCode = 1;
4938
+ return;
4939
+ }
4940
+ const { target, ext } = platformInfo;
4941
+ const binaryUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${latest}/${target}.${ext}`;
4942
+ const checksumUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${latest}/${target}.sha256`;
4943
+ const tmp = join6(tmpdir(), `elnora-update-${Date.now()}`);
4944
+ mkdirSync4(tmp, { recursive: true });
4945
+ const archivePath = join6(tmp, `${target}.${ext}`);
4946
+ try {
4947
+ console.error("Downloading...");
4948
+ await downloadToFile(binaryUrl, archivePath);
4949
+ let checksumVerified = false;
4950
+ try {
4951
+ const checksumRes = await fetch(checksumUrl, { signal: AbortSignal.timeout(15e3) });
4952
+ if (checksumRes.ok) {
4953
+ const checksumText = await checksumRes.text();
4954
+ const expected = checksumText.split(/\s/)[0].trim();
4955
+ if (expected.length === 64) {
4956
+ const actual = createHash("sha256").update(readFileSync6(archivePath)).digest("hex");
4957
+ if (actual.toLowerCase() !== expected.toLowerCase()) {
4958
+ console.error("Checksum verification failed. The download may be corrupted.");
4959
+ process.exitCode = 1;
4960
+ return;
4961
+ }
4962
+ checksumVerified = true;
4963
+ }
4964
+ }
4965
+ } catch {
4966
+ }
4967
+ if (!checksumVerified) {
4968
+ console.error("Warning: Could not verify download checksum. Proceeding anyway.");
4969
+ }
4970
+ if (process.platform === "win32") {
4971
+ const extractDir = join6(tmp, "extracted");
4972
+ execFileSync("powershell", [
4973
+ "-NoProfile",
4974
+ "-Command",
4975
+ `Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force`
4976
+ ]);
4977
+ const installDir = join6(process.env.USERPROFILE ?? process.env.HOME ?? "", ".elnora", "bin");
4978
+ mkdirSync4(installDir, { recursive: true });
4979
+ copyFileSync(join6(extractDir, target), join6(installDir, "elnora.exe"));
4980
+ } else {
4981
+ execFileSync("tar", ["-xzf", archivePath, "-C", tmp]);
4982
+ const installDir = process.env.ELNORA_INSTALL_DIR ?? join6(process.env.HOME ?? "", ".local", "bin");
4983
+ mkdirSync4(installDir, { recursive: true });
4984
+ const dest = join6(installDir, "elnora");
4985
+ copyFileSync(join6(tmp, target), dest);
4986
+ chmodSync2(dest, 493);
4987
+ }
4988
+ const done = color2 ? `${pc5.green("\u2713")} Updated to v${latest}` : `\u2713 Updated to v${latest}`;
4989
+ console.error(done);
4990
+ } finally {
4991
+ try {
4992
+ rmSync(tmp, { recursive: true, force: true });
4993
+ } catch {
4994
+ }
4995
+ }
4996
+ }
4720
4997
  function addUpdateCommand(program2) {
4721
- program2.command("update").description("Update Elnora CLI to the latest version").action(async () => {
4722
- const color = isColorEnabled();
4723
- const hint = getManualHint();
4998
+ program2.command("update").description("Check for updates (use --install to apply)").option("--install", "Download and install the update").action(async (opts) => {
4999
+ const color2 = isColorEnabled();
5000
+ const method = detectInstallMethod();
4724
5001
  let latest;
4725
5002
  try {
4726
- const res = await fetch(NPM_REGISTRY_URL2, { signal: AbortSignal.timeout(5e3) });
4727
- if (!res.ok) {
4728
- console.error("Failed to check for updates. Try manually:");
4729
- console.error(hint);
4730
- process.exit(1);
4731
- }
4732
- const data = await res.json();
4733
- latest = data.version ?? "unknown";
5003
+ latest = await fetchLatestVersion();
4734
5004
  } catch {
4735
- console.error("Failed to reach registry. Try manually:");
4736
- console.error(hint);
4737
- process.exit(1);
5005
+ console.error("Failed to check for updates.");
5006
+ process.exitCode = 1;
5007
+ return;
5008
+ }
5009
+ if (latest === "unknown") {
5010
+ console.error("Could not determine latest version.");
5011
+ process.exitCode = 1;
5012
+ return;
4738
5013
  }
4739
5014
  if (latest === VERSION || !isNewerVersion(latest, VERSION)) {
4740
- const msg2 = color ? `${pc5.green("\u2713")} Already on the latest version (v${VERSION})` : `\u2713 Already on the latest version (v${VERSION})`;
5015
+ const msg2 = color2 ? `${pc5.green("\u2713")} Already on the latest version (v${VERSION})` : `\u2713 Already on the latest version (v${VERSION})`;
4741
5016
  console.error(msg2);
4742
5017
  return;
4743
5018
  }
4744
- const msg = color ? `Updating Elnora CLI ${pc5.dim(`v${VERSION}`)} \u2192 ${pc5.green(`v${latest}`)}...` : `Updating Elnora CLI v${VERSION} \u2192 v${latest}...`;
5019
+ const instruction = getUpdateInstruction(method);
5020
+ if (!opts.install) {
5021
+ const msg2 = color2 ? `${pc5.yellow("Update available:")} v${VERSION} \u2192 v${latest}` : `Update available: v${VERSION} \u2192 v${latest}`;
5022
+ console.error(msg2);
5023
+ console.error(`Run: ${instruction}`);
5024
+ return;
5025
+ }
5026
+ const msg = color2 ? `Updating Elnora CLI ${pc5.dim(`v${VERSION}`)} \u2192 ${pc5.green(`v${latest}`)}...` : `Updating Elnora CLI v${VERSION} \u2192 v${latest}...`;
4745
5027
  console.error(msg);
4746
- const platformInfo = getPlatformTarget();
4747
- if (!platformInfo) {
4748
- console.error(`
4749
- Unsupported platform: ${process.platform}. Try manually:`);
4750
- console.error(" npm update -g @elnora-ai/cli");
4751
- process.exit(1);
5028
+ if (method === "npm") {
5029
+ console.error("Installed via npm. Run:");
5030
+ console.error(` npm update -g @elnora-ai/cli`);
5031
+ return;
5032
+ }
5033
+ if (method === "homebrew") {
5034
+ console.error("Installed via Homebrew. Run:");
5035
+ console.error(` brew upgrade elnora`);
5036
+ return;
4752
5037
  }
4753
- const { target, ext } = platformInfo;
4754
- const binaryUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${latest}/${target}.${ext}`;
4755
- const checksumUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${latest}/${target}.sha256`;
4756
- const tmp = join4(tmpdir(), `elnora-update-${Date.now()}`);
4757
- mkdirSync4(tmp, { recursive: true });
4758
- const archivePath = join4(tmp, `${target}.${ext}`);
4759
5038
  try {
4760
- await downloadToFile(binaryUrl, archivePath);
4761
- let checksumVerified = false;
4762
- try {
4763
- const checksumRes = await fetch(checksumUrl, { signal: AbortSignal.timeout(15e3) });
4764
- if (checksumRes.ok) {
4765
- const checksumText = await checksumRes.text();
4766
- const expected = checksumText.split(/\s/)[0].trim();
4767
- if (expected.length === 64) {
4768
- const actual = createHash("sha256").update(readFileSync6(archivePath)).digest("hex");
4769
- if (actual.toLowerCase() !== expected.toLowerCase()) {
4770
- console.error("\nChecksum verification failed. The download may be corrupted.");
4771
- console.error("Try again, or install manually:");
4772
- console.error(hint);
4773
- process.exit(1);
4774
- }
4775
- checksumVerified = true;
4776
- }
4777
- }
4778
- } catch {
4779
- }
4780
- if (!checksumVerified) {
4781
- console.error("Warning: Could not verify download checksum. Proceeding anyway.");
4782
- }
4783
- if (process.platform === "win32") {
4784
- const extractDir = join4(tmp, "extracted");
4785
- execFileSync("powershell", [
4786
- "-NoProfile",
4787
- "-Command",
4788
- `Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force`
4789
- ]);
4790
- const installDir = join4(process.env.USERPROFILE ?? process.env.HOME ?? "", ".elnora", "bin");
4791
- mkdirSync4(installDir, { recursive: true });
4792
- copyFileSync(join4(extractDir, target), join4(installDir, "elnora.exe"));
4793
- } else {
4794
- execFileSync("tar", ["-xzf", archivePath, "-C", tmp]);
4795
- const installDir = process.env.ELNORA_INSTALL_DIR ?? join4(process.env.HOME ?? "", ".local", "bin");
4796
- mkdirSync4(installDir, { recursive: true });
4797
- const dest = join4(installDir, "elnora");
4798
- copyFileSync(join4(tmp, target), dest);
4799
- chmodSync2(dest, 493);
4800
- }
4801
- const done = color ? `${pc5.green("\u2713")} Updated to v${latest}` : `\u2713 Updated to v${latest}`;
4802
- console.error(done);
5039
+ await installBinaryUpdate(latest);
4803
5040
  } catch {
4804
- console.error("\nUpdate failed. Try manually:");
4805
- console.error(hint);
4806
- process.exit(1);
4807
- } finally {
4808
- try {
4809
- rmSync(tmp, { recursive: true, force: true });
4810
- } catch {
4811
- }
5041
+ console.error("Update failed. Try manually:");
5042
+ console.error(
5043
+ process.platform === "win32" ? " irm https://cli.elnora.ai/install.ps1 | iex" : " curl -fsSL https://cli.elnora.ai/install.sh | bash"
5044
+ );
5045
+ process.exitCode = 1;
4812
5046
  }
4813
5047
  });
4814
5048
  }
@@ -4821,22 +5055,21 @@ function addWhoamiCommand(program2) {
4821
5055
  const parentOpts = program2.opts();
4822
5056
  const profileName = parentOpts.profile ?? "default";
4823
5057
  try {
4824
- const key = resolveApiKey(profileName);
4825
- const masked = key.length > 20 ? `${key.slice(0, 16)}...${key.slice(-4)}` : `${key.slice(0, 4)}...`;
5058
+ const hint = resolveCredentialHint(profileName);
4826
5059
  let orgName = "unknown";
4827
5060
  try {
4828
- const client = new ElnoraApiClient(key);
5061
+ const client = new ElnoraApiClient(resolveApiKey(profileName));
4829
5062
  const orgs = await client.get("organizations");
4830
5063
  const orgList = Array.isArray(orgs) ? orgs : orgs?.items ?? [];
4831
5064
  if (orgList.length > 0) orgName = orgList[0]?.name ?? "unknown";
4832
5065
  } catch {
4833
5066
  }
4834
5067
  if (parentOpts.json) {
4835
- console.log(JSON.stringify({ profile: profileName, apiKey: masked, orgName }, null, 2));
5068
+ console.log(JSON.stringify({ profile: profileName, credential: hint, orgName }, null, 2));
4836
5069
  } else {
4837
5070
  console.log(`
4838
- Profile: ${profileName}`);
4839
- console.log(`API Key: ${masked}`);
5071
+ Profile: ${profileName}`);
5072
+ console.log(`Credential: ${hint}`);
4840
5073
  console.log(`Org: ${orgName}`);
4841
5074
  console.log("");
4842
5075
  }
@@ -4925,7 +5158,7 @@ addMcpCommands(program, registry);
4925
5158
  addDoctorCommand(program);
4926
5159
  addWhoamiCommand(program);
4927
5160
  addOpenCommand(program);
4928
- addSetupClaudeCommand(program);
5161
+ addSetupCommand(program);
4929
5162
  addUpdateCommand(program);
4930
5163
  addCompletionCommand(program);
4931
5164
  registerUpdateCheck();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elnora-ai/cli",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "Elnora AI Platform CLI",
5
5
  "type": "module",
6
6
  "bin": {