@elnora-ai/cli 1.1.0 → 1.1.3

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 (5) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +155 -137
  3. package/dist/cli.js +809 -298
  4. package/package.json +66 -67
  5. package/dist/cli.cjs +0 -442
package/dist/cli.js CHANGED
@@ -10,18 +10,6 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/lib/config.ts
13
- import { readFileSync } from "node:fs";
14
- import { dirname, join } from "node:path";
15
- import { fileURLToPath } from "node:url";
16
- function getVersion() {
17
- try {
18
- const dir = dirname(fileURLToPath(import.meta.url));
19
- const pkg = JSON.parse(readFileSync(join(dir, "../../package.json"), "utf-8"));
20
- return pkg.version ?? "0.0.0-dev";
21
- } catch {
22
- return "0.0.0-dev";
23
- }
24
- }
25
13
  function buildUrl(endpointKey, params, baseUrl) {
26
14
  const path = ENDPOINTS[endpointKey];
27
15
  if (!path) throw new Error(`Unknown endpoint: ${endpointKey}`);
@@ -37,7 +25,7 @@ var VERSION, BASE_URL, AI_SERVER_URL, ENDPOINTS, DEFAULT_HEADERS;
37
25
  var init_config = __esm({
38
26
  "src/lib/config.ts"() {
39
27
  "use strict";
40
- VERSION = getVersion();
28
+ VERSION = typeof __APP_VERSION__ !== "undefined" ? __APP_VERSION__ : process.env.npm_package_version ?? "0.0.0-dev";
41
29
  BASE_URL = "https://platform.elnora.ai/api/v1";
42
30
  AI_SERVER_URL = process.env.ELNORA_AI_SERVER_URL ?? "https://platform.elnora.ai/ai-server";
43
31
  ENDPOINTS = {
@@ -175,6 +163,23 @@ function formatErrorPayload(err) {
175
163
  }
176
164
  return payload;
177
165
  }
166
+ function formatErrorForHuman(err) {
167
+ const lines = [];
168
+ const message = scrub(err.message);
169
+ lines.push(`Error: ${message}`);
170
+ if (err instanceof ElnoraError && err.suggestion) {
171
+ const steps = err.suggestion.split("\n").map((s) => s.trim()).filter(Boolean);
172
+ if (steps.length === 1) {
173
+ lines.push("", ` ${steps[0]}`);
174
+ } else {
175
+ lines.push("", "Next steps:");
176
+ for (const step of steps) {
177
+ lines.push(` ${step}`);
178
+ }
179
+ }
180
+ }
181
+ return lines.join("\n");
182
+ }
178
183
  var ElnoraError, AuthError, NotFoundError, RateLimitError, ValidationError, ServerError, EXIT_CODES, SCRUB_KEY_VALUE_RE, SCRUB_LONG_TOKEN_RE;
179
184
  var init_errors = __esm({
180
185
  "src/lib/errors.ts"() {
@@ -244,9 +249,9 @@ var init_errors = __esm({
244
249
  });
245
250
 
246
251
  // src/lib/profiles.ts
247
- import { chmodSync, existsSync, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
252
+ import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
248
253
  import { homedir } from "node:os";
249
- import { join as join2 } from "node:path";
254
+ import { join } from "node:path";
250
255
  function validateProfileName(name) {
251
256
  if (!PROFILE_NAME_RE.test(name)) {
252
257
  throw new ValidationError(
@@ -305,7 +310,7 @@ function serializeProfiles(profiles) {
305
310
  return parts.join("\n");
306
311
  }
307
312
  function writeSecureFile(filePath, content) {
308
- const dir = join2(filePath, "..");
313
+ const dir = join(filePath, "..");
309
314
  mkdirSync(dir, { recursive: true });
310
315
  if (process.platform !== "win32") {
311
316
  try {
@@ -318,7 +323,7 @@ function writeSecureFile(filePath, content) {
318
323
  function loadProfiles() {
319
324
  if (!existsSync(PROFILES_FILE)) return {};
320
325
  try {
321
- const text = readFileSync2(PROFILES_FILE, "utf-8");
326
+ const text = readFileSync(PROFILES_FILE, "utf-8");
322
327
  return parseProfilesToml(text);
323
328
  } catch {
324
329
  return {};
@@ -368,7 +373,7 @@ function migrateConfigIfNeeded() {
368
373
  if (!existsSync(LEGACY_CONFIG_FILE)) return false;
369
374
  let apiKey = "";
370
375
  try {
371
- const text = readFileSync2(LEGACY_CONFIG_FILE, "utf-8");
376
+ const text = readFileSync(LEGACY_CONFIG_FILE, "utf-8");
372
377
  for (const line of text.split("\n")) {
373
378
  const trimmed = line.trim();
374
379
  if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
@@ -396,9 +401,9 @@ var init_profiles = __esm({
396
401
  "src/lib/profiles.ts"() {
397
402
  "use strict";
398
403
  init_errors();
399
- CONFIG_DIR = join2(homedir(), ".elnora");
400
- PROFILES_FILE = join2(CONFIG_DIR, "profiles.toml");
401
- LEGACY_CONFIG_FILE = join2(CONFIG_DIR, "config.toml");
404
+ CONFIG_DIR = join(homedir(), ".elnora");
405
+ PROFILES_FILE = join(CONFIG_DIR, "profiles.toml");
406
+ LEGACY_CONFIG_FILE = join(CONFIG_DIR, "config.toml");
402
407
  PROFILE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;
403
408
  }
404
409
  });
@@ -544,14 +549,30 @@ var init_client = __esm({
544
549
  bodyText = scrub(await response.text());
545
550
  } catch {
546
551
  }
547
- if (response.status === 401 || response.status === 403) {
548
- throw new AuthError(bodyText.slice(0, 200) || "Authentication failed");
552
+ let errorMessage = bodyText;
553
+ try {
554
+ const parsed = JSON.parse(bodyText);
555
+ if (parsed.messages?.length) {
556
+ errorMessage = parsed.messages.join(" ");
557
+ } else if (parsed.message) {
558
+ errorMessage = parsed.message;
559
+ }
560
+ } catch {
561
+ }
562
+ if (response.status === 401) {
563
+ throw new AuthError(errorMessage.slice(0, 200) || "Authentication failed");
564
+ }
565
+ if (response.status === 403) {
566
+ throw new ElnoraError(errorMessage.slice(0, 200) || "Access denied", {
567
+ code: "FORBIDDEN",
568
+ suggestion: "You don't have permission to access this resource. Check the resource ID or your organization membership."
569
+ });
549
570
  }
550
571
  if (response.status === 404) {
551
- throw new NotFoundError("resource", bodyText.slice(0, 200) || "not found");
572
+ throw new NotFoundError("resource", errorMessage.slice(0, 200) || "not found");
552
573
  }
553
574
  if (response.status === 422) {
554
- throw new ValidationError(bodyText.slice(0, 500) || "Validation error");
575
+ throw new ValidationError(errorMessage.slice(0, 500) || "Validation error");
555
576
  }
556
577
  if (response.status === 429) {
557
578
  if (attempt < MAX_RETRIES) {
@@ -824,6 +845,7 @@ var init_http_server = __esm({
824
845
 
825
846
  // src/adapters/cli.ts
826
847
  init_client();
848
+ init_config();
827
849
  init_errors();
828
850
  import { Command } from "commander";
829
851
 
@@ -979,12 +1001,12 @@ function kebabToCamel(str) {
979
1001
  return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
980
1002
  }
981
1003
  function buildProgram(registry2) {
982
- const program2 = new Command().name("elnora").description("Elnora AI Platform CLI").version("0.0.1").option("--compact", "Token-efficient minimal output", false).option("--output <format>", "Output format (json, csv)", "json").option("--fields <fields>", "Comma-separated fields to include").option("--profile <name>", "Named profile to use").option("--json", "Force JSON output", false);
1004
+ const program2 = new Command().name("elnora").description("Elnora AI Platform CLI").version(VERSION).option("--compact", "Token-efficient minimal output", false).option("--output <format>", "Output format (json, csv)", "json").option("--fields <fields>", "Comma-separated fields to include").option("--profile <name>", "Named profile to use").option("--json", "Force JSON output", false);
983
1005
  for (const group of registry2.groups()) {
984
1006
  const groupCmd = new Command(group).description(`Manage ${group}`);
985
1007
  const commands = registry2.byGroup(group);
986
1008
  for (const cmd of commands) {
987
- const subName = cmd.name.split(".").slice(1).join("-");
1009
+ const subName = camelToKebab(cmd.name.split(".").slice(1).join("-"));
988
1010
  const sub = new Command(subName).description(cmd.description);
989
1011
  const opts = zodToCommanderOptions(cmd.inputSchema);
990
1012
  for (const opt of opts) {
@@ -1024,7 +1046,11 @@ function buildProgram(registry2) {
1024
1046
  }
1025
1047
  }
1026
1048
  const profileName = parentOpts.profile ?? "default";
1027
- const client = ElnoraApiClient.fromEnv(profileName);
1049
+ if (parentOpts.profile && !("profile" in input && input.profile !== "default")) {
1050
+ input.profile = parentOpts.profile;
1051
+ }
1052
+ const skipAuth = cmd.name.startsWith("auth.login") || cmd.name === "auth.logout" || cmd.name === "auth.profiles";
1053
+ const client = skipAuth ? new ElnoraApiClient("placeholder") : ElnoraApiClient.fromEnv(profileName);
1028
1054
  const ctx = {
1029
1055
  client,
1030
1056
  profileName,
@@ -1037,19 +1063,37 @@ function buildProgram(registry2) {
1037
1063
  };
1038
1064
  const parsed = cmd.inputSchema.parse(input);
1039
1065
  const result = await cmd.execute(parsed, ctx);
1040
- const formatted = formatOutput(result, {
1041
- format: ctx.output.format,
1042
- compact: ctx.output.compact,
1043
- fields: ctx.output.fields
1044
- });
1045
- process.stdout.write(`${formatted}
1066
+ if (result != null) {
1067
+ const isStreamed = typeof result === "object" && result.streamed === true;
1068
+ if (!isStreamed) {
1069
+ const hasGlobalFlags = ctx.output.compact || ctx.output.format !== "json" || ctx.output.fields;
1070
+ let formatted;
1071
+ if (cmd.formatOutput && !hasGlobalFlags) {
1072
+ formatted = cmd.formatOutput(result, ctx.output.format);
1073
+ } else {
1074
+ formatted = formatOutput(result, {
1075
+ format: ctx.output.format,
1076
+ compact: ctx.output.compact,
1077
+ fields: ctx.output.fields
1078
+ });
1079
+ }
1080
+ if (formatted) {
1081
+ process.stdout.write(`${formatted}
1046
1082
  `);
1083
+ }
1084
+ }
1085
+ }
1047
1086
  } catch (err) {
1048
1087
  const error = err instanceof Error ? err : new Error(String(err));
1049
- const payload = formatErrorPayload(error);
1050
- process.stderr.write(`${JSON.stringify(payload, null, 2)}
1088
+ if (process.stderr.isTTY) {
1089
+ process.stderr.write(`${formatErrorForHuman(error)}
1051
1090
  `);
1052
- process.exit(getExitCode(error));
1091
+ } else {
1092
+ const payload = formatErrorPayload(error);
1093
+ process.stderr.write(`${JSON.stringify(payload, null, 2)}
1094
+ `);
1095
+ }
1096
+ process.exitCode = getExitCode(error);
1053
1097
  }
1054
1098
  });
1055
1099
  groupCmd.addCommand(sub);
@@ -1454,7 +1498,7 @@ var paginationInput = {
1454
1498
 
1455
1499
  // src/commands/audit/list.ts
1456
1500
  var inputSchema15 = z16.object({
1457
- org: z16.string().uuid().describe("Organization ID"),
1501
+ orgId: z16.string().uuid().describe("Organization ID"),
1458
1502
  ...paginationInput,
1459
1503
  action: z16.string().optional().describe("Filter by action type"),
1460
1504
  userId: z16.string().optional().describe("Filter by user ID")
@@ -1474,7 +1518,7 @@ var auditList = {
1474
1518
  if (input.action) queryParams.action = input.action;
1475
1519
  if (input.userId) queryParams.userId = input.userId;
1476
1520
  return ctx.client.get("audit_log", {
1477
- pathParams: { orgId: input.org },
1521
+ pathParams: { orgId: input.orgId },
1478
1522
  queryParams
1479
1523
  });
1480
1524
  },
@@ -1494,6 +1538,78 @@ init_client();
1494
1538
  init_errors();
1495
1539
  init_profiles();
1496
1540
  import { z as z17 } from "zod";
1541
+
1542
+ // src/lib/prompt.ts
1543
+ import { createInterface as createInterface2 } from "node:readline";
1544
+ function promptSecret(message) {
1545
+ return new Promise((resolve, reject) => {
1546
+ const { stdin, stdout } = process;
1547
+ if (!stdin.isTTY) {
1548
+ const rl = createInterface2({ input: stdin, output: stdout });
1549
+ rl.question(message, (answer) => {
1550
+ rl.close();
1551
+ resolve(answer.trim());
1552
+ });
1553
+ return;
1554
+ }
1555
+ stdout.write(message);
1556
+ let input = "";
1557
+ const wasRaw = stdin.isRaw;
1558
+ stdin.setRawMode(true);
1559
+ stdin.resume();
1560
+ stdin.setEncoding("utf8");
1561
+ const onData = (char) => {
1562
+ switch (char) {
1563
+ case "\n":
1564
+ case "\r":
1565
+ case "": {
1566
+ stdin.setRawMode(wasRaw);
1567
+ stdin.pause();
1568
+ stdin.removeListener("data", onData);
1569
+ stdout.write("\n");
1570
+ resolve(input.trim());
1571
+ break;
1572
+ }
1573
+ case "":
1574
+ // Ctrl+C
1575
+ case "\x1B": {
1576
+ stdin.setRawMode(wasRaw);
1577
+ stdin.pause();
1578
+ stdin.removeListener("data", onData);
1579
+ stdout.write("\n");
1580
+ reject(new Error("Cancelled"));
1581
+ break;
1582
+ }
1583
+ case "\x7F":
1584
+ case "\b": {
1585
+ if (input.length > 0) {
1586
+ input = input.slice(0, -1);
1587
+ stdout.write("\b \b");
1588
+ }
1589
+ break;
1590
+ }
1591
+ default: {
1592
+ input += char;
1593
+ stdout.write("*");
1594
+ break;
1595
+ }
1596
+ }
1597
+ };
1598
+ stdin.on("data", onData);
1599
+ });
1600
+ }
1601
+
1602
+ // src/lib/tty.ts
1603
+ function isTTY() {
1604
+ return Boolean(process.stdout.isTTY);
1605
+ }
1606
+ function isColorEnabled() {
1607
+ if (process.env.NO_COLOR !== void 0) return false;
1608
+ if (process.env.FORCE_COLOR !== void 0) return true;
1609
+ return isTTY();
1610
+ }
1611
+
1612
+ // src/commands/auth/login.ts
1497
1613
  var inputSchema16 = z17.object({
1498
1614
  apiKey: z17.string().optional().describe("Elnora API key (elnora_live_...)"),
1499
1615
  profile: z17.string().default("default").describe("Profile name to save key under")
@@ -1505,30 +1621,118 @@ var authLogin = {
1505
1621
  inputSchema: inputSchema16,
1506
1622
  outputSchema: z17.any(),
1507
1623
  async execute(input, _ctx) {
1508
- const apiKey = input.apiKey;
1624
+ let apiKey = input.apiKey;
1625
+ const profileName = input.profile;
1626
+ validateProfileName(profileName);
1627
+ const existing = listProfileNames();
1628
+ const profileExists = existing.includes(profileName);
1629
+ if (!apiKey) {
1630
+ if (profileExists) {
1631
+ let savedKey;
1632
+ try {
1633
+ savedKey = getApiKey(profileName);
1634
+ } catch {
1635
+ }
1636
+ if (savedKey && isTTY()) {
1637
+ process.stderr.write(`
1638
+ Checking profile "${profileName}"...`);
1639
+ try {
1640
+ const client = new ElnoraApiClient(savedKey);
1641
+ const result = await client.get("projects", {
1642
+ queryParams: { page: 1, pageSize: 1 }
1643
+ });
1644
+ const projectCount = result?.totalCount ?? result?.items?.length ?? 0;
1645
+ process.stderr.write(` authenticated (${projectCount} project${projectCount !== 1 ? "s" : ""}).
1646
+
1647
+ `);
1648
+ process.stderr.write(" To update your API key, run:\n");
1649
+ process.stderr.write(
1650
+ ` elnora auth login --api-key <new-key>${profileName !== "default" ? ` --profile ${profileName}` : ""}
1651
+
1652
+ `
1653
+ );
1654
+ return null;
1655
+ } catch {
1656
+ process.stderr.write(" key is invalid or expired.\n\n");
1657
+ }
1658
+ }
1659
+ }
1660
+ if (isTTY()) {
1661
+ if (profileExists) {
1662
+ process.stderr.write(` Enter a new API key for profile "${profileName}"
1663
+ `);
1664
+ } else {
1665
+ process.stderr.write("\n Set up Elnora CLI authentication\n");
1666
+ }
1667
+ process.stderr.write(" Get your API key at: https://platform.elnora.ai > Settings > API Keys\n\n");
1668
+ try {
1669
+ apiKey = await promptSecret(" API key (Ctrl+C to cancel): ");
1670
+ } catch {
1671
+ process.stderr.write("\n Login cancelled.\n\n");
1672
+ process.exit(0);
1673
+ }
1674
+ process.stderr.write("\n");
1675
+ }
1676
+ }
1509
1677
  if (!apiKey) {
1510
1678
  throw new ValidationError(
1511
- "API key is required. Use --api-key flag to provide it.",
1679
+ "API key is required. Use --api-key flag or run interactively in a terminal.",
1512
1680
  "Get your API key from: https://platform.elnora.ai > Settings > API Keys"
1513
1681
  );
1514
1682
  }
1515
1683
  const key = apiKey.trim();
1516
1684
  if (!key.startsWith("elnora_live_")) {
1517
- throw new AuthError("API key must start with 'elnora_live_'.");
1685
+ throw new AuthError("API key must start with 'elnora_live_'. Check that you copied the full key.", {
1686
+ suggestion: "Get your API key from: https://platform.elnora.ai > Settings > API Keys"
1687
+ });
1518
1688
  }
1519
1689
  if (key.length < 20) {
1520
- throw new AuthError("API key looks too short. Check your key and try again.");
1690
+ throw new AuthError("API key looks too short. Make sure you copied the full key.", {
1691
+ suggestion: "Get your API key from: https://platform.elnora.ai > Settings > API Keys"
1692
+ });
1693
+ }
1694
+ if (isTTY()) {
1695
+ process.stderr.write(" Verifying...");
1696
+ }
1697
+ try {
1698
+ const tempClient = new ElnoraApiClient(key);
1699
+ const result = await tempClient.get("projects", {
1700
+ queryParams: { page: 1, pageSize: 1 }
1701
+ });
1702
+ const projectCount = result?.totalCount ?? result?.items?.length ?? 0;
1703
+ const configPath = saveProfile(profileName, key);
1704
+ if (isTTY()) {
1705
+ process.stderr.write(` Done! ${projectCount} project${projectCount !== 1 ? "s" : ""} accessible.
1706
+ `);
1707
+ process.stderr.write(` Saved to profile "${profileName}" at ${configPath}
1708
+
1709
+ `);
1710
+ }
1711
+ return { profile: profileName, verified: true, configPath };
1712
+ } catch (err) {
1713
+ if (isTTY()) {
1714
+ process.stderr.write("\n");
1715
+ }
1716
+ if (err instanceof AuthError) {
1717
+ throw new AuthError("API key is invalid or has been revoked. Please check your key and try again.", {
1718
+ suggestion: "Get a new key from: https://platform.elnora.ai > Settings > API Keys"
1719
+ });
1720
+ }
1721
+ throw err;
1521
1722
  }
1522
- const profileName = input.profile;
1523
- validateProfileName(profileName);
1524
- const tempClient = new ElnoraApiClient(key);
1525
- await tempClient.get("projects", { queryParams: { page: 1, pageSize: 1 } });
1526
- const configPath = saveProfile(profileName, key);
1527
- return { profile: profileName, verified: true, configPath };
1528
1723
  },
1529
1724
  formatOutput(output, format) {
1530
- if (format === "compact") return output.profile ?? JSON.stringify(output);
1531
- return JSON.stringify(output, null, 2);
1725
+ const data = output;
1726
+ if (format === "compact") return data.profile ?? JSON.stringify(output);
1727
+ const lines = [
1728
+ `\u2713 Authenticated! API key saved to profile "${data.profile}".`,
1729
+ "",
1730
+ "Next steps:",
1731
+ " elnora projects list See your projects",
1732
+ " elnora tasks create --project <ID> --stream Start a conversation",
1733
+ " elnora doctor Verify your setup"
1734
+ ];
1735
+ return lines.join("\n");
1532
1736
  }
1533
1737
  };
1534
1738
 
@@ -1772,26 +1976,102 @@ function addCompletionCommand(program2) {
1772
1976
  init_client();
1773
1977
  init_config();
1774
1978
  init_profiles();
1775
- import { existsSync as existsSync2, statSync } from "node:fs";
1776
- import pc from "picocolors";
1979
+ import { existsSync as existsSync3, statSync } from "node:fs";
1980
+ import pc2 from "picocolors";
1777
1981
 
1778
- // src/lib/tty.ts
1779
- function isTTY() {
1780
- return Boolean(process.stdout.isTTY);
1982
+ // src/lib/update-check.ts
1983
+ init_config();
1984
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1985
+ import { homedir as homedir2 } from "node:os";
1986
+ import { join as join2 } from "node:path";
1987
+ import pc from "picocolors";
1988
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@elnora-ai/cli/latest";
1989
+ var CACHE_DIR = join2(homedir2(), ".elnora");
1990
+ var CACHE_FILE = join2(CACHE_DIR, ".update-check");
1991
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1992
+ var FETCH_TIMEOUT_MS = 3e3;
1993
+ function shouldSkip() {
1994
+ if (process.env.CI || process.env.GITHUB_ACTIONS) return true;
1995
+ if (process.env.ELNORA_NO_UPDATE_CHECK) return true;
1996
+ if (!process.stderr.isTTY) return true;
1997
+ return false;
1781
1998
  }
1782
- function isColorEnabled() {
1783
- if (process.env.NO_COLOR !== void 0) return false;
1784
- if (process.env.FORCE_COLOR !== void 0) return true;
1785
- return isTTY();
1999
+ function readCache() {
2000
+ try {
2001
+ if (!existsSync2(CACHE_FILE)) return null;
2002
+ const raw = readFileSync2(CACHE_FILE, "utf-8");
2003
+ return JSON.parse(raw);
2004
+ } catch {
2005
+ return null;
2006
+ }
2007
+ }
2008
+ function writeCache(entry) {
2009
+ try {
2010
+ mkdirSync2(CACHE_DIR, { recursive: true });
2011
+ writeFileSync2(CACHE_FILE, JSON.stringify(entry), "utf-8");
2012
+ } catch {
2013
+ }
2014
+ }
2015
+ function showUpdateNotice(latest) {
2016
+ const color = isColorEnabled();
2017
+ const msg = color ? `
2018
+ ${pc.yellow(`Update available: v${VERSION} \u2192 v${latest}`)}
2019
+ Run: curl -fsSL https://cli.elnora.ai/install.sh | bash
2020
+ ` : `
2021
+ Update available: v${VERSION} \u2192 v${latest}
2022
+ Run: curl -fsSL https://cli.elnora.ai/install.sh | bash
2023
+ `;
2024
+ process.stderr.write(msg);
2025
+ }
2026
+ function isNewerVersion(a, b) {
2027
+ const pa = a.split(".").map(Number);
2028
+ const pb = b.split(".").map(Number);
2029
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
2030
+ const na = pa[i] ?? 0;
2031
+ const nb = pb[i] ?? 0;
2032
+ if (na > nb) return true;
2033
+ if (na < nb) return false;
2034
+ }
2035
+ return false;
2036
+ }
2037
+ function registerUpdateCheck() {
2038
+ if (shouldSkip()) return;
2039
+ const cached = readCache();
2040
+ if (cached) {
2041
+ const elapsed = Date.now() - new Date(cached.checkedAt).getTime();
2042
+ if (elapsed < CHECK_INTERVAL_MS) {
2043
+ if (cached.latest && cached.latest !== VERSION && cached.latest !== "unknown") {
2044
+ if (isNewerVersion(cached.latest, VERSION)) {
2045
+ showUpdateNotice(cached.latest);
2046
+ }
2047
+ }
2048
+ return;
2049
+ }
2050
+ }
2051
+ setTimeout(async () => {
2052
+ try {
2053
+ const response = await fetch(NPM_REGISTRY_URL, {
2054
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2055
+ });
2056
+ if (!response.ok) return;
2057
+ const data = await response.json();
2058
+ const latest = data.version ?? "unknown";
2059
+ writeCache({ checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latest });
2060
+ if (latest !== VERSION && latest !== "unknown" && isNewerVersion(latest, VERSION)) {
2061
+ showUpdateNotice(latest);
2062
+ }
2063
+ } catch {
2064
+ }
2065
+ }, 0);
1786
2066
  }
1787
2067
 
1788
2068
  // src/commands/doctor.ts
1789
2069
  function addDoctorCommand(program2) {
1790
2070
  program2.command("doctor").description("Check Elnora CLI configuration and connectivity").action(async () => {
1791
2071
  const color = isColorEnabled();
1792
- const ok = (msg) => color ? ` ${pc.green("\u2713")} ${msg}` : ` \u2713 ${msg}`;
1793
- const fail = (msg) => color ? ` ${pc.red("\u2717")} ${msg}` : ` \u2717 ${msg}`;
1794
- const warn = (msg) => color ? ` ${pc.yellow("!")} ${msg}` : ` ! ${msg}`;
2072
+ const ok = (msg) => color ? ` ${pc2.green("\u2713")} ${msg}` : ` \u2713 ${msg}`;
2073
+ const fail = (msg) => color ? ` ${pc2.red("\u2717")} ${msg}` : ` \u2717 ${msg}`;
2074
+ const warn = (msg) => color ? ` ${pc2.yellow("!")} ${msg}` : ` ! ${msg}`;
1795
2075
  console.error(`
1796
2076
  Elnora CLI v${VERSION}
1797
2077
  `);
@@ -1832,7 +2112,7 @@ Elnora CLI v${VERSION}
1832
2112
  if (res.ok) {
1833
2113
  const data = await res.json();
1834
2114
  const latest = data.version ?? "unknown";
1835
- if (latest === VERSION || latest === "unknown") {
2115
+ if (latest === VERSION || latest === "unknown" || !isNewerVersion(latest, VERSION)) {
1836
2116
  console.error(ok(`Version current v${VERSION} (latest: v${latest})`));
1837
2117
  } else {
1838
2118
  console.error(warn(`Update available v${VERSION} \u2192 v${latest}`));
@@ -1850,7 +2130,7 @@ Elnora CLI v${VERSION}
1850
2130
  if (process.platform === "win32") {
1851
2131
  console.error(ok("Config permissions (skipped on Windows)"));
1852
2132
  passed++;
1853
- } else if (!existsSync2(PROFILES_FILE)) {
2133
+ } else if (!existsSync3(PROFILES_FILE)) {
1854
2134
  console.error(warn("Config permissions ~/.elnora/profiles.toml not found"));
1855
2135
  } else {
1856
2136
  const mode = statSync(PROFILES_FILE).mode & 511;
@@ -1878,7 +2158,7 @@ Elnora CLI v${VERSION}
1878
2158
  console.error("All checks passed.");
1879
2159
  } else {
1880
2160
  console.error(`${passed}/${total} checks passed.`);
1881
- if (!existsSync2(PROFILES_FILE)) console.error("\nRun: elnora auth login");
2161
+ if (!existsSync3(PROFILES_FILE)) console.error("\nRun: elnora auth login");
1882
2162
  }
1883
2163
  });
1884
2164
  }
@@ -2752,7 +3032,7 @@ var healthCheck = {
2752
3032
  // src/commands/library/create-folder.ts
2753
3033
  import { z as z51 } from "zod";
2754
3034
  var inputSchema49 = z51.object({
2755
- org: z51.string().uuid().describe("Organization ID"),
3035
+ orgId: z51.string().uuid().describe("Organization ID"),
2756
3036
  name: z51.string().min(1).describe("Folder name"),
2757
3037
  parent: z51.string().uuid().optional().describe("Parent folder ID")
2758
3038
  });
@@ -2766,7 +3046,7 @@ var libraryCreateFolder = {
2766
3046
  const body = { name: input.name };
2767
3047
  if (input.parent) body.parentId = input.parent;
2768
3048
  return ctx.client.post("library_folders", body, {
2769
- pathParams: { orgId: input.org }
3049
+ pathParams: { orgId: input.orgId }
2770
3050
  });
2771
3051
  },
2772
3052
  formatOutput(output, format) {
@@ -2778,7 +3058,7 @@ var libraryCreateFolder = {
2778
3058
  // src/commands/library/delete-folder.ts
2779
3059
  import { z as z52 } from "zod";
2780
3060
  var inputSchema50 = z52.object({
2781
- org: z52.string().uuid().describe("Organization ID"),
3061
+ orgId: z52.string().uuid().describe("Organization ID"),
2782
3062
  folderId: z52.string().uuid().describe("Folder ID to delete")
2783
3063
  });
2784
3064
  var libraryDeleteFolder = {
@@ -2790,7 +3070,7 @@ var libraryDeleteFolder = {
2790
3070
  annotations: { destructiveHint: true },
2791
3071
  async execute(input, ctx) {
2792
3072
  await ctx.client.del("library_folder", {
2793
- pathParams: { orgId: input.org, id: input.folderId }
3073
+ pathParams: { orgId: input.orgId, id: input.folderId }
2794
3074
  });
2795
3075
  return { deleted: true, folderId: input.folderId };
2796
3076
  },
@@ -2803,7 +3083,7 @@ var libraryDeleteFolder = {
2803
3083
  // src/commands/library/files.ts
2804
3084
  import { z as z53 } from "zod";
2805
3085
  var inputSchema51 = z53.object({
2806
- org: z53.string().uuid().describe("Organization ID"),
3086
+ orgId: z53.string().uuid().describe("Organization ID"),
2807
3087
  ...paginationInput
2808
3088
  });
2809
3089
  var libraryFiles = {
@@ -2815,7 +3095,7 @@ var libraryFiles = {
2815
3095
  annotations: { readOnlyHint: true },
2816
3096
  async execute(input, ctx) {
2817
3097
  return ctx.client.get("library_files", {
2818
- pathParams: { orgId: input.org },
3098
+ pathParams: { orgId: input.orgId },
2819
3099
  queryParams: { page: input.page, pageSize: input.pageSize }
2820
3100
  });
2821
3101
  },
@@ -2828,7 +3108,7 @@ var libraryFiles = {
2828
3108
  // src/commands/library/folders.ts
2829
3109
  import { z as z54 } from "zod";
2830
3110
  var inputSchema52 = z54.object({
2831
- org: z54.string().uuid().describe("Organization ID")
3111
+ orgId: z54.string().uuid().describe("Organization ID")
2832
3112
  });
2833
3113
  var libraryFolders = {
2834
3114
  name: "library.folders",
@@ -2839,7 +3119,7 @@ var libraryFolders = {
2839
3119
  annotations: { readOnlyHint: true },
2840
3120
  async execute(input, ctx) {
2841
3121
  return ctx.client.get("library_folders", {
2842
- pathParams: { orgId: input.org }
3122
+ pathParams: { orgId: input.orgId }
2843
3123
  });
2844
3124
  },
2845
3125
  formatOutput(output, format) {
@@ -2851,7 +3131,7 @@ var libraryFolders = {
2851
3131
  // src/commands/library/rename-folder.ts
2852
3132
  import { z as z55 } from "zod";
2853
3133
  var inputSchema53 = z55.object({
2854
- org: z55.string().uuid().describe("Organization ID"),
3134
+ orgId: z55.string().uuid().describe("Organization ID"),
2855
3135
  folderId: z55.string().uuid().describe("Folder ID to rename"),
2856
3136
  name: z55.string().min(1).describe("New folder name")
2857
3137
  });
@@ -2866,7 +3146,7 @@ var libraryRenameFolder = {
2866
3146
  "library_folder",
2867
3147
  { name: input.name },
2868
3148
  {
2869
- pathParams: { orgId: input.org, id: input.folderId }
3149
+ pathParams: { orgId: input.orgId, id: input.folderId }
2870
3150
  }
2871
3151
  );
2872
3152
  },
@@ -3174,9 +3454,10 @@ var orgsInvite = {
3174
3454
  pathParams: { orgId: input.orgId }
3175
3455
  });
3176
3456
  const items = Array.isArray(existing) ? existing : existing?.items ?? [];
3177
- const match = items.find(
3178
- (inv) => inv.email?.toLowerCase() === input.email.toLowerCase() && inv.status === "Pending"
3179
- );
3457
+ const match = items.find((inv) => {
3458
+ const i = inv;
3459
+ return i.email?.toLowerCase() === input.email.toLowerCase() && i.status === "Pending";
3460
+ });
3180
3461
  if (match) return match;
3181
3462
  return ctx.client.post(
3182
3463
  "org_invitations",
@@ -3781,6 +4062,114 @@ function registerSearchCommands() {
3781
4062
  return [searchTasks, searchFiles, searchAll, searchFileContent];
3782
4063
  }
3783
4064
 
4065
+ // src/commands/setup-claude.ts
4066
+ init_config();
4067
+ import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
4068
+ import { homedir as homedir3 } from "node:os";
4069
+ import { dirname, join as join3 } from "node:path";
4070
+ import { fileURLToPath } from "node:url";
4071
+ import pc3 from "picocolors";
4072
+ var __script_dir;
4073
+ try {
4074
+ __script_dir = dirname(fileURLToPath(import.meta.url));
4075
+ } catch {
4076
+ __script_dir = typeof __dirname !== "undefined" ? __dirname : process.cwd();
4077
+ }
4078
+ var ELNORA_DIR = join3(homedir3(), ".elnora");
4079
+ var PLUGIN_DIR = join3(ELNORA_DIR, "plugin", "elnora");
4080
+ var CLAUDE_DIR = join3(homedir3(), ".claude");
4081
+ var MARKETPLACES_FILE = join3(CLAUDE_DIR, "plugins", "known_marketplaces.json");
4082
+ var SETTINGS_FILE = join3(CLAUDE_DIR, "settings.json");
4083
+ var MARKETPLACE_NAME = "elnora-local";
4084
+ var PLUGIN_ID = `elnora@${MARKETPLACE_NAME}`;
4085
+ function findSkillsDir() {
4086
+ const candidates = [
4087
+ join3(__script_dir, "../../skills"),
4088
+ join3(__script_dir, "../skills"),
4089
+ // When running from source via tsx
4090
+ join3(__script_dir, "../../../skills"),
4091
+ // Standalone binary: skills extracted to ~/.elnora/skills/
4092
+ join3(ELNORA_DIR, "skills")
4093
+ ];
4094
+ for (const dir of candidates) {
4095
+ if (existsSync4(join3(dir, "elnora-platform", "SKILL.md"))) {
4096
+ return dir;
4097
+ }
4098
+ }
4099
+ return null;
4100
+ }
4101
+ function readJsonFile(path) {
4102
+ try {
4103
+ return JSON.parse(readFileSync5(path, "utf-8"));
4104
+ } catch {
4105
+ return {};
4106
+ }
4107
+ }
4108
+ function writeJsonFile(path, data) {
4109
+ mkdirSync3(join3(path, ".."), { recursive: true });
4110
+ writeFileSync3(path, `${JSON.stringify(data, null, 2)}
4111
+ `, "utf-8");
4112
+ }
4113
+ function addSetupClaudeCommand(program2) {
4114
+ program2.command("setup-claude").description("Install Elnora skills for Claude Code").action(() => {
4115
+ const color = isColorEnabled();
4116
+ const ok = (msg) => color ? ` ${pc3.green("\u2713")} ${msg}` : ` \u2713 ${msg}`;
4117
+ const fail = (msg) => color ? ` ${pc3.red("\u2717")} ${msg}` : ` \u2717 ${msg}`;
4118
+ console.error("");
4119
+ if (!existsSync4(CLAUDE_DIR)) {
4120
+ console.error(fail("Claude Code not found (~/.claude/ does not exist)"));
4121
+ console.error("");
4122
+ console.error(" Install Claude Code first: https://claude.ai/code");
4123
+ process.exit(1);
4124
+ }
4125
+ console.error(ok("Claude Code found"));
4126
+ const skillsDir = findSkillsDir();
4127
+ if (!skillsDir) {
4128
+ console.error(fail("Skills not found"));
4129
+ console.error("");
4130
+ console.error(" Skills may not be bundled with your install method.");
4131
+ console.error(" Try: npm install -g @elnora-ai/cli");
4132
+ process.exit(1);
4133
+ }
4134
+ console.error(ok(`Skills found at ${skillsDir}`));
4135
+ mkdirSync3(PLUGIN_DIR, { recursive: true });
4136
+ writeJsonFile(join3(PLUGIN_DIR, ".claude-plugin", "plugin.json"), {
4137
+ name: "elnora",
4138
+ version: VERSION,
4139
+ description: "AI-powered bioprotocol generation and lab workflow management. Connect to the Elnora AI Platform to generate, optimize, and manage bioprotocols.",
4140
+ author: { name: "Elnora AI", url: "https://elnora.ai" },
4141
+ repository: "https://github.com/Elnora-AI/elnora-cli",
4142
+ homepage: "https://elnora.ai",
4143
+ license: "Apache-2.0",
4144
+ keywords: ["bioprotocol", "lab", "pharma", "life-sciences"]
4145
+ });
4146
+ cpSync(skillsDir, join3(PLUGIN_DIR, "skills"), { recursive: true });
4147
+ console.error(ok("Skills installed to ~/.elnora/plugin/"));
4148
+ const marketplaces = readJsonFile(MARKETPLACES_FILE);
4149
+ marketplaces[MARKETPLACE_NAME] = {
4150
+ source: {
4151
+ source: "directory",
4152
+ path: join3(ELNORA_DIR, "plugin")
4153
+ },
4154
+ installLocation: join3(ELNORA_DIR, "plugin"),
4155
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
4156
+ };
4157
+ writeJsonFile(MARKETPLACES_FILE, marketplaces);
4158
+ console.error(ok("Marketplace registered in Claude Code"));
4159
+ const settings = readJsonFile(SETTINGS_FILE);
4160
+ if (!settings.enabledPlugins || typeof settings.enabledPlugins !== "object") {
4161
+ settings.enabledPlugins = {};
4162
+ }
4163
+ settings.enabledPlugins[PLUGIN_ID] = true;
4164
+ writeJsonFile(SETTINGS_FILE, settings);
4165
+ console.error(ok("Plugin enabled"));
4166
+ console.error("");
4167
+ console.error(" Elnora skills are now available in Claude Code.");
4168
+ console.error(" Restart Claude Code if it's currently running.");
4169
+ console.error("");
4170
+ });
4171
+ }
4172
+
3784
4173
  // src/commands/tasks/archive.ts
3785
4174
  import { z as z88 } from "zod";
3786
4175
  var inputSchema86 = z88.object({
@@ -3807,128 +4196,69 @@ var tasksArchive = {
3807
4196
 
3808
4197
  // src/commands/tasks/create.ts
3809
4198
  import { z as z89 } from "zod";
3810
- var inputSchema87 = z89.object({
3811
- project: z89.string().uuid().describe("Project ID"),
3812
- title: z89.string().optional().describe("Task title"),
3813
- message: z89.string().optional().describe("Initial message")
3814
- });
3815
- var tasksCreate = {
3816
- name: "tasks.create",
3817
- group: "tasks",
3818
- description: "Create a new task in a project",
3819
- inputSchema: inputSchema87,
3820
- outputSchema: z89.any(),
3821
- async execute(input, ctx) {
3822
- return ctx.client.post("tasks", {
3823
- projectId: input.project,
3824
- title: input.title,
3825
- initialMessage: input.message
3826
- });
3827
- },
3828
- formatOutput(output, format) {
3829
- if (format === "compact") return output.id ?? JSON.stringify(output);
3830
- return JSON.stringify(output, null, 2);
3831
- }
3832
- };
3833
4199
 
3834
- // src/commands/tasks/get.ts
3835
- import { z as z90 } from "zod";
3836
- var inputSchema88 = z90.object({
3837
- taskId: z90.string().uuid().describe("Task ID")
3838
- });
3839
- var tasksGet = {
3840
- name: "tasks.get",
3841
- group: "tasks",
3842
- description: "Get details of a specific task",
3843
- inputSchema: inputSchema88,
3844
- outputSchema: z90.any(),
3845
- annotations: { readOnlyHint: true },
3846
- async execute(input, ctx) {
3847
- return ctx.client.get("task", {
3848
- pathParams: { id: input.taskId }
4200
+ // src/lib/poll.ts
4201
+ init_errors();
4202
+ async function pollForResponse(client, taskId, afterSequence = 0, timeoutMs = 12e4) {
4203
+ const deadline = Date.now() + timeoutMs;
4204
+ while (Date.now() < deadline) {
4205
+ const messages = await client.get("task_messages", {
4206
+ pathParams: { id: taskId },
4207
+ queryParams: { limit: 10 }
3849
4208
  });
3850
- },
3851
- formatOutput(output, format) {
3852
- if (format === "compact") return output.id ?? JSON.stringify(output);
3853
- return JSON.stringify(output, null, 2);
4209
+ const items = Array.isArray(messages) ? messages : messages?.items ?? [];
4210
+ if (Array.isArray(items)) {
4211
+ const assistantMsg = items.find((m) => {
4212
+ const msg = m;
4213
+ return (msg.role === "assistant" || msg.role === "Assistant") && (msg.sequence ?? msg.sequenceNumber ?? 0) > afterSequence;
4214
+ });
4215
+ if (assistantMsg) return assistantMsg;
4216
+ }
4217
+ await new Promise((r) => setTimeout(r, 2e3));
3854
4218
  }
3855
- };
3856
-
3857
- // src/commands/tasks/list.ts
3858
- import { z as z91 } from "zod";
3859
- var inputSchema89 = z91.object({
3860
- project: z91.string().uuid().optional().describe("Project ID to filter by"),
3861
- ...paginationInput
3862
- });
3863
- var tasksList = {
3864
- name: "tasks.list",
3865
- group: "tasks",
3866
- description: "List tasks, optionally filtered by project",
3867
- inputSchema: inputSchema89,
3868
- outputSchema: z91.any(),
3869
- annotations: { readOnlyHint: true },
3870
- async execute(input, ctx) {
3871
- if (input.project) {
3872
- return ctx.client.get("project_tasks", {
3873
- pathParams: { id: input.project },
3874
- queryParams: { page: input.page, pageSize: input.pageSize }
3875
- });
3876
- }
3877
- return ctx.client.get("tasks", {
3878
- queryParams: { page: input.page, pageSize: input.pageSize }
3879
- });
3880
- },
3881
- formatOutput(output, format) {
3882
- if (format === "compact") return output.id ?? JSON.stringify(output);
3883
- return JSON.stringify(output, null, 2);
3884
- }
3885
- };
3886
-
3887
- // src/commands/tasks/messages.ts
3888
- import { z as z92 } from "zod";
3889
- var inputSchema90 = z92.object({
3890
- taskId: z92.string().uuid().describe("Task ID"),
3891
- cursor: z92.string().optional().describe("Pagination cursor"),
3892
- limit: z92.number().int().min(1).max(100).default(50).describe("Results per page")
3893
- });
3894
- var tasksMessages = {
3895
- name: "tasks.messages",
3896
- group: "tasks",
3897
- description: "List messages for a task",
3898
- inputSchema: inputSchema90,
3899
- outputSchema: z92.any(),
3900
- annotations: { readOnlyHint: true },
3901
- async execute(input, ctx) {
3902
- const queryParams = { limit: input.limit };
3903
- if (input.cursor) {
3904
- queryParams.cursor = input.cursor;
3905
- }
3906
- return ctx.client.get("task_messages", {
3907
- pathParams: { id: input.taskId },
3908
- queryParams
3909
- });
3910
- },
3911
- formatOutput(output, format) {
3912
- if (format === "compact") return output.id ?? JSON.stringify(output);
3913
- return JSON.stringify(output, null, 2);
3914
- }
3915
- };
4219
+ throw new ElnoraError("Timed out waiting for agent response (120s)", {
4220
+ code: "RESPONSE_TIMEOUT",
4221
+ suggestion: "The agent may still be processing. Check with: elnora tasks messages <task-id>"
4222
+ });
4223
+ }
3916
4224
 
3917
- // src/commands/tasks/send.ts
3918
- init_errors();
4225
+ // src/commands/tasks/create.ts
3919
4226
  init_profiles();
3920
- import { z as z93 } from "zod";
3921
4227
 
3922
4228
  // src/lib/stream.ts
3923
4229
  init_config();
4230
+ async function getStreamToken(taskId, apiKey) {
4231
+ const url = `${BASE_URL}/auth/stream-token`;
4232
+ const response = await fetch(url, {
4233
+ method: "POST",
4234
+ headers: {
4235
+ ...DEFAULT_HEADERS,
4236
+ "X-API-Key": apiKey
4237
+ },
4238
+ body: JSON.stringify({ taskId }),
4239
+ signal: AbortSignal.timeout(1e4)
4240
+ });
4241
+ if (!response.ok) {
4242
+ throw new Error(`Stream token exchange failed: HTTP ${response.status}`);
4243
+ }
4244
+ const data = await response.json();
4245
+ return data.token;
4246
+ }
3924
4247
  var TERMINAL_EVENTS = /* @__PURE__ */ new Set(["completed", "error", "timeout"]);
3925
4248
  var STREAM_TIMEOUT_MS = 3e5;
3926
4249
  async function* streamTask(taskId, apiKey, options) {
4250
+ let streamToken;
4251
+ try {
4252
+ streamToken = await getStreamToken(taskId, apiKey);
4253
+ } catch (err) {
4254
+ yield { type: "error", content: `Failed to get stream token: ${err instanceof Error ? err.message : String(err)}` };
4255
+ return;
4256
+ }
3927
4257
  const baseUrl = options?.aiServerBaseUrl ?? AI_SERVER_URL;
3928
4258
  const url = `${baseUrl}/api/v1/tasks/${taskId}/stream`;
3929
4259
  const response = await fetch(url, {
3930
4260
  headers: {
3931
- "X-API-Key": apiKey,
4261
+ Authorization: `Bearer ${streamToken}`,
3932
4262
  Accept: "text/event-stream"
3933
4263
  },
3934
4264
  signal: options?.signal ?? AbortSignal.timeout(STREAM_TIMEOUT_MS)
@@ -3987,7 +4317,7 @@ async function collectStreamResponse(taskId, apiKey, options) {
3987
4317
  }
3988
4318
 
3989
4319
  // src/lib/stream-renderer.ts
3990
- import pc2 from "picocolors";
4320
+ import pc4 from "picocolors";
3991
4321
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3992
4322
  var SPINNER_INTERVAL_MS = 80;
3993
4323
  var StreamRenderer = class {
@@ -4010,7 +4340,7 @@ var StreamRenderer = class {
4010
4340
  case "tool_end": {
4011
4341
  this.stopSpinner();
4012
4342
  const toolEvent = event;
4013
- const icon = toolEvent.success ? this.useColor ? pc2.green("\u2713") : "\u2713" : this.useColor ? pc2.red("\u2717") : "\u2717";
4343
+ const icon = toolEvent.success ? this.useColor ? pc4.green("\u2713") : "\u2713" : this.useColor ? pc4.red("\u2717") : "\u2717";
4014
4344
  const suffix = toolEvent.result ? ` (${toolEvent.result})` : "";
4015
4345
  process.stderr.write(` ${icon} ${toolEvent.tool}${suffix}
4016
4346
  `);
@@ -4019,7 +4349,7 @@ var StreamRenderer = class {
4019
4349
  case "progress":
4020
4350
  this.stopSpinner();
4021
4351
  process.stderr.write(
4022
- ` ${this.useColor ? pc2.dim(event.content) : event.content}
4352
+ ` ${this.useColor ? pc4.dim(event.content) : event.content}
4023
4353
  `
4024
4354
  );
4025
4355
  break;
@@ -4035,14 +4365,14 @@ var StreamRenderer = class {
4035
4365
  this.stopSpinner();
4036
4366
  process.stderr.write(
4037
4367
  `
4038
- ${this.useColor ? pc2.red("Error:") : "Error:"} ${event.content}
4368
+ ${this.useColor ? pc4.red("Error:") : "Error:"} ${event.content}
4039
4369
  `
4040
4370
  );
4041
4371
  break;
4042
4372
  case "timeout":
4043
4373
  this.stopSpinner();
4044
4374
  process.stderr.write(`
4045
- ${this.useColor ? pc2.yellow("Stream timed out.") : "Stream timed out."}
4375
+ ${this.useColor ? pc4.yellow("Stream timed out.") : "Stream timed out."}
4046
4376
  `);
4047
4377
  break;
4048
4378
  }
@@ -4064,7 +4394,7 @@ ${this.useColor ? pc2.yellow("Stream timed out.") : "Stream timed out."}
4064
4394
  }
4065
4395
  writeSpinnerFrame() {
4066
4396
  const frame = SPINNER_FRAMES[this.spinnerFrame];
4067
- const text = this.useColor ? pc2.dim(`${frame} ${this.currentLabel}`) : `${frame} ${this.currentLabel}`;
4397
+ const text = this.useColor ? pc4.dim(`${frame} ${this.currentLabel}`) : `${frame} ${this.currentLabel}`;
4068
4398
  process.stderr.write(`\r ${text}`);
4069
4399
  }
4070
4400
  stopSpinner() {
@@ -4078,7 +4408,145 @@ ${this.useColor ? pc2.yellow("Stream timed out.") : "Stream timed out."}
4078
4408
  }
4079
4409
  };
4080
4410
 
4411
+ // src/commands/tasks/create.ts
4412
+ var inputSchema87 = z89.object({
4413
+ project: z89.string().uuid().describe("Project ID"),
4414
+ title: z89.string().optional().describe("Task title"),
4415
+ message: z89.string().optional().describe("Initial message"),
4416
+ wait: z89.boolean().default(false).describe("Wait for agent response (polling)"),
4417
+ stream: z89.boolean().default(false).describe("Stream agent response in real-time (SSE)")
4418
+ });
4419
+ var tasksCreate = {
4420
+ name: "tasks.create",
4421
+ group: "tasks",
4422
+ description: "Create a new task in a project",
4423
+ inputSchema: inputSchema87,
4424
+ outputSchema: z89.any(),
4425
+ async execute(input, ctx) {
4426
+ const result = await ctx.client.post("tasks", {
4427
+ projectId: input.project,
4428
+ title: input.title,
4429
+ initialMessage: input.message
4430
+ });
4431
+ const taskId = result.id;
4432
+ const sequence = result?.sequence ?? result?.sequenceNumber ?? 0;
4433
+ if (!input.message || !input.stream && !input.wait) {
4434
+ return result;
4435
+ }
4436
+ if (ctx.mode === "mcp") {
4437
+ try {
4438
+ const apiKey = resolveApiKey(ctx.profileName);
4439
+ const content = await collectStreamResponse(taskId, apiKey);
4440
+ return { ...result, response: content };
4441
+ } catch {
4442
+ return pollForResponse(ctx.client, taskId, sequence);
4443
+ }
4444
+ }
4445
+ if (input.stream) {
4446
+ const apiKey = resolveApiKey(ctx.profileName);
4447
+ const renderer = new StreamRenderer();
4448
+ for await (const event of streamTask(taskId, apiKey)) {
4449
+ renderer.renderEvent(event);
4450
+ }
4451
+ renderer.stopSpinner();
4452
+ return { ...result, streamed: true };
4453
+ }
4454
+ return pollForResponse(ctx.client, taskId, sequence);
4455
+ },
4456
+ formatOutput(output, format) {
4457
+ const data = output;
4458
+ if (format === "compact") return data.id ?? JSON.stringify(output);
4459
+ if (data.streamed) return "";
4460
+ return JSON.stringify(output, null, 2);
4461
+ }
4462
+ };
4463
+
4464
+ // src/commands/tasks/get.ts
4465
+ import { z as z90 } from "zod";
4466
+ var inputSchema88 = z90.object({
4467
+ taskId: z90.string().uuid().describe("Task ID")
4468
+ });
4469
+ var tasksGet = {
4470
+ name: "tasks.get",
4471
+ group: "tasks",
4472
+ description: "Get details of a specific task",
4473
+ inputSchema: inputSchema88,
4474
+ outputSchema: z90.any(),
4475
+ annotations: { readOnlyHint: true },
4476
+ async execute(input, ctx) {
4477
+ return ctx.client.get("task", {
4478
+ pathParams: { id: input.taskId }
4479
+ });
4480
+ },
4481
+ formatOutput(output, format) {
4482
+ if (format === "compact") return output.id ?? JSON.stringify(output);
4483
+ return JSON.stringify(output, null, 2);
4484
+ }
4485
+ };
4486
+
4487
+ // src/commands/tasks/list.ts
4488
+ import { z as z91 } from "zod";
4489
+ var inputSchema89 = z91.object({
4490
+ project: z91.string().uuid().optional().describe("Project ID to filter by"),
4491
+ ...paginationInput
4492
+ });
4493
+ var tasksList = {
4494
+ name: "tasks.list",
4495
+ group: "tasks",
4496
+ description: "List tasks, optionally filtered by project",
4497
+ inputSchema: inputSchema89,
4498
+ outputSchema: z91.any(),
4499
+ annotations: { readOnlyHint: true },
4500
+ async execute(input, ctx) {
4501
+ if (input.project) {
4502
+ return ctx.client.get("project_tasks", {
4503
+ pathParams: { id: input.project },
4504
+ queryParams: { page: input.page, pageSize: input.pageSize }
4505
+ });
4506
+ }
4507
+ return ctx.client.get("tasks", {
4508
+ queryParams: { page: input.page, pageSize: input.pageSize }
4509
+ });
4510
+ },
4511
+ formatOutput(output, format) {
4512
+ if (format === "compact") return output.id ?? JSON.stringify(output);
4513
+ return JSON.stringify(output, null, 2);
4514
+ }
4515
+ };
4516
+
4517
+ // src/commands/tasks/messages.ts
4518
+ import { z as z92 } from "zod";
4519
+ var inputSchema90 = z92.object({
4520
+ taskId: z92.string().uuid().describe("Task ID"),
4521
+ cursor: z92.string().optional().describe("Pagination cursor"),
4522
+ limit: z92.number().int().min(1).max(100).default(50).describe("Results per page")
4523
+ });
4524
+ var tasksMessages = {
4525
+ name: "tasks.messages",
4526
+ group: "tasks",
4527
+ description: "List messages for a task",
4528
+ inputSchema: inputSchema90,
4529
+ outputSchema: z92.any(),
4530
+ annotations: { readOnlyHint: true },
4531
+ async execute(input, ctx) {
4532
+ const queryParams = { limit: input.limit };
4533
+ if (input.cursor) {
4534
+ queryParams.cursor = input.cursor;
4535
+ }
4536
+ return ctx.client.get("task_messages", {
4537
+ pathParams: { id: input.taskId },
4538
+ queryParams
4539
+ });
4540
+ },
4541
+ formatOutput(output, format) {
4542
+ if (format === "compact") return output.id ?? JSON.stringify(output);
4543
+ return JSON.stringify(output, null, 2);
4544
+ }
4545
+ };
4546
+
4081
4547
  // src/commands/tasks/send.ts
4548
+ import { z as z93 } from "zod";
4549
+ init_profiles();
4082
4550
  var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4083
4551
  var inputSchema91 = z93.object({
4084
4552
  taskId: z93.string().uuid().describe("Task ID"),
@@ -4087,28 +4555,6 @@ var inputSchema91 = z93.object({
4087
4555
  wait: z93.boolean().default(false).describe("Wait for agent response (polling)"),
4088
4556
  stream: z93.boolean().default(false).describe("Stream agent response in real-time (SSE)")
4089
4557
  });
4090
- async function pollForResponse(client, taskId, userMessageSequence, timeoutMs = 12e4) {
4091
- const deadline = Date.now() + timeoutMs;
4092
- while (Date.now() < deadline) {
4093
- const messages = await client.get("task_messages", {
4094
- pathParams: { id: taskId },
4095
- queryParams: { limit: 10 }
4096
- });
4097
- const items = Array.isArray(messages) ? messages : messages?.items ?? [];
4098
- if (Array.isArray(items)) {
4099
- const assistantMsg = items.find((m) => {
4100
- const msg = m;
4101
- return (msg.role === "assistant" || msg.role === "Assistant") && (msg.sequence ?? msg.sequenceNumber ?? 0) > userMessageSequence;
4102
- });
4103
- if (assistantMsg) return assistantMsg;
4104
- }
4105
- await new Promise((r) => setTimeout(r, 2e3));
4106
- }
4107
- throw new ElnoraError("Timed out waiting for agent response (120s)", {
4108
- code: "RESPONSE_TIMEOUT",
4109
- suggestion: "The agent may still be processing. Check with: elnora tasks messages <task-id>"
4110
- });
4111
- }
4112
4558
  var tasksSend = {
4113
4559
  name: "tasks.send",
4114
4560
  group: "tasks",
@@ -4154,7 +4600,9 @@ var tasksSend = {
4154
4600
  return pollForResponse(ctx.client, input.taskId, sequence);
4155
4601
  },
4156
4602
  formatOutput(output, format) {
4157
- if (format === "compact") return output.id ?? JSON.stringify(output);
4603
+ const data = output;
4604
+ if (format === "compact") return data.id ?? JSON.stringify(output);
4605
+ if (data.streamed) return "";
4158
4606
  return JSON.stringify(output, null, 2);
4159
4607
  }
4160
4608
  };
@@ -4197,6 +4645,136 @@ function registerTaskCommands() {
4197
4645
  return [tasksList, tasksGet, tasksCreate, tasksSend, tasksMessages, tasksUpdate, tasksArchive];
4198
4646
  }
4199
4647
 
4648
+ // src/commands/update.ts
4649
+ init_config();
4650
+ import { execFileSync } from "node:child_process";
4651
+ import { createHash } from "node:crypto";
4652
+ import { chmodSync as chmodSync2, copyFileSync, createWriteStream, mkdirSync as mkdirSync4, readFileSync as readFileSync6, rmSync } from "node:fs";
4653
+ import { tmpdir } from "node:os";
4654
+ import { join as join4 } from "node:path";
4655
+ import { Readable } from "node:stream";
4656
+ import { pipeline } from "node:stream/promises";
4657
+ import pc5 from "picocolors";
4658
+ var NPM_REGISTRY_URL2 = "https://registry.npmjs.org/@elnora-ai/cli/latest";
4659
+ var GITHUB_REPO = "Elnora-AI/elnora-cli";
4660
+ function getPlatformTarget() {
4661
+ const arch = process.arch === "arm64" ? "arm64" : "x64";
4662
+ switch (process.platform) {
4663
+ case "darwin":
4664
+ return { target: `elnora-macos-${arch}`, ext: "tar.gz" };
4665
+ case "linux":
4666
+ return { target: `elnora-linux-${arch}`, ext: "tar.gz" };
4667
+ case "win32":
4668
+ return { target: `elnora-win-${arch}.exe`, ext: "zip" };
4669
+ default:
4670
+ return null;
4671
+ }
4672
+ }
4673
+ function getManualHint() {
4674
+ return process.platform === "win32" ? " irm https://cli.elnora.ai/install.ps1 | iex" : " curl -fsSL https://cli.elnora.ai/install.sh | bash";
4675
+ }
4676
+ async function downloadToFile(url, dest) {
4677
+ const res = await fetch(url, { signal: AbortSignal.timeout(12e4), redirect: "follow" });
4678
+ if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
4679
+ const nodeStream = Readable.fromWeb(res.body);
4680
+ await pipeline(nodeStream, createWriteStream(dest));
4681
+ }
4682
+ function addUpdateCommand(program2) {
4683
+ program2.command("update").description("Update Elnora CLI to the latest version").action(async () => {
4684
+ const color = isColorEnabled();
4685
+ const hint = getManualHint();
4686
+ let latest;
4687
+ try {
4688
+ const res = await fetch(NPM_REGISTRY_URL2, { signal: AbortSignal.timeout(5e3) });
4689
+ if (!res.ok) {
4690
+ console.error("Failed to check for updates. Try manually:");
4691
+ console.error(hint);
4692
+ process.exit(1);
4693
+ }
4694
+ const data = await res.json();
4695
+ latest = data.version ?? "unknown";
4696
+ } catch {
4697
+ console.error("Failed to reach registry. Try manually:");
4698
+ console.error(hint);
4699
+ process.exit(1);
4700
+ }
4701
+ if (latest === VERSION || !isNewerVersion(latest, VERSION)) {
4702
+ const msg2 = color ? `${pc5.green("\u2713")} Already on the latest version (v${VERSION})` : `\u2713 Already on the latest version (v${VERSION})`;
4703
+ console.error(msg2);
4704
+ return;
4705
+ }
4706
+ const msg = color ? `Updating Elnora CLI ${pc5.dim(`v${VERSION}`)} \u2192 ${pc5.green(`v${latest}`)}...` : `Updating Elnora CLI v${VERSION} \u2192 v${latest}...`;
4707
+ console.error(msg);
4708
+ const platformInfo = getPlatformTarget();
4709
+ if (!platformInfo) {
4710
+ console.error(`
4711
+ Unsupported platform: ${process.platform}. Try manually:`);
4712
+ console.error(" npm update -g @elnora-ai/cli");
4713
+ process.exit(1);
4714
+ }
4715
+ const { target, ext } = platformInfo;
4716
+ const binaryUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${latest}/${target}.${ext}`;
4717
+ const checksumUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${latest}/${target}.sha256`;
4718
+ const tmp = join4(tmpdir(), `elnora-update-${Date.now()}`);
4719
+ mkdirSync4(tmp, { recursive: true });
4720
+ const archivePath = join4(tmp, `${target}.${ext}`);
4721
+ try {
4722
+ await downloadToFile(binaryUrl, archivePath);
4723
+ let checksumVerified = false;
4724
+ try {
4725
+ const checksumRes = await fetch(checksumUrl, { signal: AbortSignal.timeout(15e3) });
4726
+ if (checksumRes.ok) {
4727
+ const checksumText = await checksumRes.text();
4728
+ const expected = checksumText.split(/\s/)[0].trim();
4729
+ if (expected.length === 64) {
4730
+ const actual = createHash("sha256").update(readFileSync6(archivePath)).digest("hex");
4731
+ if (actual.toLowerCase() !== expected.toLowerCase()) {
4732
+ console.error("\nChecksum verification failed. The download may be corrupted.");
4733
+ console.error("Try again, or install manually:");
4734
+ console.error(hint);
4735
+ process.exit(1);
4736
+ }
4737
+ checksumVerified = true;
4738
+ }
4739
+ }
4740
+ } catch {
4741
+ }
4742
+ if (!checksumVerified) {
4743
+ console.error("Warning: Could not verify download checksum. Proceeding anyway.");
4744
+ }
4745
+ if (process.platform === "win32") {
4746
+ const extractDir = join4(tmp, "extracted");
4747
+ execFileSync("powershell", [
4748
+ "-NoProfile",
4749
+ "-Command",
4750
+ `Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force`
4751
+ ]);
4752
+ const installDir = join4(process.env.USERPROFILE ?? process.env.HOME ?? "", ".elnora", "bin");
4753
+ mkdirSync4(installDir, { recursive: true });
4754
+ copyFileSync(join4(extractDir, target), join4(installDir, "elnora.exe"));
4755
+ } else {
4756
+ execFileSync("tar", ["-xzf", archivePath, "-C", tmp]);
4757
+ const installDir = process.env.ELNORA_INSTALL_DIR ?? join4(process.env.HOME ?? "", ".local", "bin");
4758
+ mkdirSync4(installDir, { recursive: true });
4759
+ const dest = join4(installDir, "elnora");
4760
+ copyFileSync(join4(tmp, target), dest);
4761
+ chmodSync2(dest, 493);
4762
+ }
4763
+ const done = color ? `${pc5.green("\u2713")} Updated to v${latest}` : `\u2713 Updated to v${latest}`;
4764
+ console.error(done);
4765
+ } catch {
4766
+ console.error("\nUpdate failed. Try manually:");
4767
+ console.error(hint);
4768
+ process.exit(1);
4769
+ } finally {
4770
+ try {
4771
+ rmSync(tmp, { recursive: true, force: true });
4772
+ } catch {
4773
+ }
4774
+ }
4775
+ });
4776
+ }
4777
+
4200
4778
  // src/commands/whoami.ts
4201
4779
  init_client();
4202
4780
  init_profiles();
@@ -4261,95 +4839,26 @@ var CommandRegistry = class {
4261
4839
 
4262
4840
  // src/cli.ts
4263
4841
  init_errors();
4264
-
4265
- // src/lib/update-check.ts
4266
- init_config();
4267
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
4268
- import { homedir as homedir2 } from "node:os";
4269
- import { join as join3 } from "node:path";
4270
- import pc3 from "picocolors";
4271
- var NPM_REGISTRY_URL = "https://registry.npmjs.org/@elnora-ai/cli/latest";
4272
- var CACHE_DIR = join3(homedir2(), ".elnora");
4273
- var CACHE_FILE = join3(CACHE_DIR, ".update-check");
4274
- var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
4275
- var FETCH_TIMEOUT_MS = 3e3;
4276
- function shouldSkip() {
4277
- if (process.env.CI || process.env.GITHUB_ACTIONS) return true;
4278
- if (process.env.ELNORA_NO_UPDATE_CHECK) return true;
4279
- if (!process.stderr.isTTY) return true;
4280
- return false;
4281
- }
4282
- function readCache() {
4283
- try {
4284
- if (!existsSync3(CACHE_FILE)) return null;
4285
- const raw = readFileSync5(CACHE_FILE, "utf-8");
4286
- return JSON.parse(raw);
4287
- } catch {
4288
- return null;
4289
- }
4290
- }
4291
- function writeCache(entry) {
4292
- try {
4293
- mkdirSync2(CACHE_DIR, { recursive: true });
4294
- writeFileSync2(CACHE_FILE, JSON.stringify(entry), "utf-8");
4295
- } catch {
4296
- }
4297
- }
4298
- function showUpdateNotice(latest) {
4299
- const color = isColorEnabled();
4300
- const msg = color ? `
4301
- ${pc3.yellow(`Update available: v${VERSION} \u2192 v${latest}`)}
4302
- Run: curl -fsSL https://cli.elnora.ai/install.sh | bash
4303
- ` : `
4304
- Update available: v${VERSION} \u2192 v${latest}
4305
- Run: curl -fsSL https://cli.elnora.ai/install.sh | bash
4306
- `;
4307
- process.stderr.write(msg);
4308
- }
4309
- function registerUpdateCheck() {
4310
- if (shouldSkip()) return;
4311
- const cached = readCache();
4312
- if (cached) {
4313
- const elapsed = Date.now() - new Date(cached.checkedAt).getTime();
4314
- if (elapsed < CHECK_INTERVAL_MS) {
4315
- if (cached.latest && cached.latest !== VERSION && cached.latest !== "unknown") {
4316
- if (cached.latest > VERSION) {
4317
- showUpdateNotice(cached.latest);
4318
- }
4319
- }
4320
- return;
4321
- }
4322
- }
4323
- setTimeout(async () => {
4324
- try {
4325
- const response = await fetch(NPM_REGISTRY_URL, {
4326
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
4327
- });
4328
- if (!response.ok) return;
4329
- const data = await response.json();
4330
- const latest = data.version ?? "unknown";
4331
- writeCache({ checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latest });
4332
- if (latest !== VERSION && latest !== "unknown" && latest > VERSION) {
4333
- showUpdateNotice(latest);
4334
- }
4335
- } catch {
4336
- }
4337
- }, 0);
4338
- }
4339
-
4340
- // src/cli.ts
4341
4842
  process.on("uncaughtException", (err) => {
4342
- const payload = formatErrorPayload(err);
4343
- process.stderr.write(`${JSON.stringify(payload, null, 2)}
4843
+ if (process.stderr.isTTY) {
4844
+ process.stderr.write(`${formatErrorForHuman(err)}
4845
+ `);
4846
+ } else {
4847
+ process.stderr.write(`${JSON.stringify(formatErrorPayload(err), null, 2)}
4344
4848
  `);
4345
- process.exit(getExitCode(err));
4849
+ }
4850
+ process.exitCode = getExitCode(err);
4346
4851
  });
4347
4852
  process.on("unhandledRejection", (reason) => {
4348
4853
  const err = reason instanceof Error ? reason : new Error(String(reason));
4349
- const payload = formatErrorPayload(err);
4350
- process.stderr.write(`${JSON.stringify(payload, null, 2)}
4854
+ if (process.stderr.isTTY) {
4855
+ process.stderr.write(`${formatErrorForHuman(err)}
4351
4856
  `);
4352
- process.exit(1);
4857
+ } else {
4858
+ process.stderr.write(`${JSON.stringify(formatErrorPayload(err), null, 2)}
4859
+ `);
4860
+ }
4861
+ process.exitCode = 1;
4353
4862
  });
4354
4863
  var registry = new CommandRegistry();
4355
4864
  var commandGroups = [
@@ -4378,6 +4887,8 @@ addMcpCommands(program, registry);
4378
4887
  addDoctorCommand(program);
4379
4888
  addWhoamiCommand(program);
4380
4889
  addOpenCommand(program);
4890
+ addSetupClaudeCommand(program);
4891
+ addUpdateCommand(program);
4381
4892
  addCompletionCommand(program);
4382
4893
  registerUpdateCheck();
4383
4894
  program.parseAsync(process.argv);