@elvix.is/sdk 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -125,8 +125,8 @@ elvix ships first-class agent support. Three surfaces:
125
125
  {
126
126
  "mcpServers": {
127
127
  "elvix": {
128
- "command": "bunx",
129
- "args": ["@elvix.is/sdk", "elvix-mcp"],
128
+ "command": "npx",
129
+ "args": ["-y", "-p", "@elvix.is/sdk", "elvix", "mcp"],
130
130
  "env": { "ELVIX_API_KEY": "eak_..." }
131
131
  }
132
132
  }
@@ -135,6 +135,20 @@ elvix ships first-class agent support. Three surfaces:
135
135
 
136
136
  Read-only by default. `--admin` opts in to mutation tools. Never logs the bearer token.
137
137
 
138
+ ## CLI
139
+
140
+ The package ships an `elvix` command:
141
+
142
+ ```bash
143
+ # Diagnose an integration — base URL, clientId, verify endpoint, API key.
144
+ ELVIX_CLIENT_ID=client_… npx -p @elvix.is/sdk elvix doctor
145
+
146
+ # Launch the MCP server on stdio.
147
+ ELVIX_API_KEY=eak_… npx -p @elvix.is/sdk elvix mcp
148
+ ```
149
+
150
+ `elvix doctor` prints a green/red checklist so "why isn't elvix working" is a two-second answer. (`elvix-mcp` is kept as an alias for `elvix mcp`.)
151
+
138
152
  Full agent guide: <https://elvix.is/docs/agents>
139
153
 
140
154
  ## Components
@@ -142,9 +156,10 @@ Full agent guide: <https://elvix.is/docs/agents>
142
156
  Every `<Elvix*>` component the SDK ships. Drop-in React, brand chord from `<ElvixProvider>`, no prop drilling.
143
157
 
144
158
  - Primitives: `ElvixCard`, `ElvixProvider`
145
- - Sign-in: `ElvixSignIn`, `ElvixSignInButton`, `ElvixRecoverGate`
159
+ - Sign-in: `ElvixSignIn`
146
160
  - Identity: `ElvixUsername`, `ElvixIdentityForm`, `ElvixAvatar`, `ElvixBanner`, `ElvixRegion`, `ElvixLanguages`
147
161
  - Account: `ElvixAddressBook`, `ElvixLegalEntities`, `ElvixSessions`, `ElvixExport`, `ElvixDeactivate`, `ElvixLeave`
162
+ - Hooks: `useElvixApp()`, `useElvixContext()`
148
163
 
149
164
  Full catalog with previews: <https://elvix.is/docs/components>
150
165
 
@@ -7,6 +7,11 @@ import {
7
7
  } from "@modelcontextprotocol/sdk/types.js";
8
8
  var DEFAULT_BASE_URL = "https://elvix.is";
9
9
  var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
10
+ function splitEndpoint(endpoint) {
11
+ const idx = endpoint.indexOf(" ");
12
+ if (idx === -1) return { method: "GET", path: endpoint };
13
+ return { method: endpoint.slice(0, idx), path: endpoint.slice(idx + 1) };
14
+ }
10
15
  function toolName(method, path) {
11
16
  return `${method.toLowerCase()}_${path.replace(/^\/api\//, "").replace(/[\/{}]+/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "")}`;
12
17
  }
@@ -15,9 +20,9 @@ async function createElvixMcpServer(opts) {
15
20
  const readonly = opts.readonly ?? true;
16
21
  const manifestRes = await fetch(`${baseUrl}/openapi.roles.json`);
17
22
  const manifest = await manifestRes.json();
18
- const tools = manifest.endpoints.filter((e) => e.role === "api").filter((e) => readonly ? SAFE_METHODS.has(e.method.toUpperCase()) : true).map((e) => ({
19
- name: toolName(e.method, e.path),
20
- description: `${e.summary ?? `${e.method} ${e.path}`}${e.adminScope ? " (requires admin scope)" : ""}`,
23
+ const tools = manifest.filter((e) => e.role === "api").map((e) => ({ entry: e, ...splitEndpoint(e.endpoint) })).filter(({ method }) => readonly ? SAFE_METHODS.has(method.toUpperCase()) : true).map(({ entry, method, path }) => ({
24
+ name: toolName(method, path),
25
+ description: `${entry.summary ?? `${method} ${path}`}${entry.adminScope ? " (requires admin scope)" : ""}`,
21
26
  inputSchema: {
22
27
  type: "object",
23
28
  properties: {
@@ -27,7 +32,7 @@ async function createElvixMcpServer(opts) {
27
32
  },
28
33
  required: ["path"]
29
34
  },
30
- _meta: { method: e.method, path: e.path, adminScope: e.adminScope ?? false }
35
+ _meta: { method, path, adminScope: entry.adminScope ?? false }
31
36
  }));
32
37
  const server = new Server(
33
38
  { name: "elvix", version: "0.1.0" },
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `elvix doctor` — diagnose an elvix integration from the terminal.
3
+ *
4
+ * Runs a green/red checklist so a developer (or their agent) can
5
+ * answer "why isn't elvix working" in two seconds instead of a support
6
+ * email. Read-only; never mutates anything.
7
+ *
8
+ * Checks:
9
+ * - base URL reachable (GET /llms.txt)
10
+ * - clientId resolves (GET /api/v1/bootstrap/<clientId> → 200)
11
+ * - verify endpoint live (POST /api/v1/verify with a dummy token → 401)
12
+ * - ELVIX_API_KEY present (informational)
13
+ *
14
+ * Inputs (env or flags):
15
+ * ELVIX_BASE_URL default https://elvix.is (--base-url=)
16
+ * ELVIX_CLIENT_ID the app's public clientId (--client-id=)
17
+ * ELVIX_API_KEY server API key (presence only)
18
+ */
19
+ declare function runDoctor(): Promise<number>;
20
+
21
+ export { runDoctor };
@@ -0,0 +1,78 @@
1
+ // src/cli/doctor.ts
2
+ var DEFAULT_BASE_URL = "https://elvix.is";
3
+ function flag(name) {
4
+ const hit = process.argv.find((a) => a.startsWith(`--${name}=`));
5
+ return hit?.split("=").slice(1).join("=");
6
+ }
7
+ async function timed(fn, timeoutMs = 8e3) {
8
+ const ctrl = new AbortController();
9
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
10
+ try {
11
+ return await fn();
12
+ } catch {
13
+ return null;
14
+ } finally {
15
+ clearTimeout(t);
16
+ }
17
+ }
18
+ async function runDoctor() {
19
+ const baseUrl = flag("base-url") ?? process.env.ELVIX_BASE_URL ?? DEFAULT_BASE_URL;
20
+ const clientId = flag("client-id") ?? process.env.ELVIX_CLIENT_ID;
21
+ const apiKey = process.env.ELVIX_API_KEY;
22
+ const checks = [];
23
+ const llms = await timed(() => fetch(`${baseUrl}/llms.txt`));
24
+ checks.push({
25
+ label: `Base URL reachable (${baseUrl})`,
26
+ ok: Boolean(llms?.ok),
27
+ detail: llms ? `HTTP ${llms.status}` : "no response / timeout"
28
+ });
29
+ if (clientId) {
30
+ const boot = await timed(
31
+ () => fetch(`${baseUrl}/api/v1/bootstrap/${encodeURIComponent(clientId)}`)
32
+ );
33
+ checks.push({
34
+ label: `clientId resolves (${clientId})`,
35
+ ok: Boolean(boot?.ok),
36
+ detail: boot ? boot.ok ? `HTTP ${boot.status}` : `HTTP ${boot.status} \u2014 wrong clientId, or app is draft/deleted` : "no response / timeout"
37
+ });
38
+ } else {
39
+ checks.push({
40
+ label: "clientId resolves",
41
+ ok: false,
42
+ detail: "skipped \u2014 pass --client-id=<id> or set ELVIX_CLIENT_ID"
43
+ });
44
+ }
45
+ const verify = await timed(
46
+ () => fetch(`${baseUrl}/api/v1/verify`, {
47
+ method: "POST",
48
+ headers: { "content-type": "application/json" },
49
+ body: JSON.stringify({ token: "doctor-probe" })
50
+ })
51
+ );
52
+ checks.push({
53
+ label: "Verify endpoint live (POST /api/v1/verify)",
54
+ ok: verify?.status === 401,
55
+ detail: verify ? `HTTP ${verify.status} (401 expected)` : "no response / timeout"
56
+ });
57
+ checks.push({
58
+ label: "ELVIX_API_KEY present",
59
+ ok: Boolean(apiKey),
60
+ detail: apiKey ? `set (${apiKey.slice(0, 8)}\u2026)` : "not set \u2014 server-side verify will fail"
61
+ });
62
+ let criticalFail = false;
63
+ process.stdout.write("\nelvix doctor\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
64
+ for (const c of checks) {
65
+ const mark = c.ok ? "\u2713" : "\u2717";
66
+ process.stdout.write(` ${mark} ${c.label}
67
+ ${c.detail}
68
+ `);
69
+ }
70
+ if (!checks[0]?.ok || !checks[2]?.ok) criticalFail = true;
71
+ process.stdout.write(
72
+ criticalFail ? "\nResult: elvix is NOT reachable from here. Check network / base URL.\n" : "\nResult: elvix is reachable. Warnings above (if any) are config hints.\n"
73
+ );
74
+ return criticalFail ? 1 : 0;
75
+ }
76
+ export {
77
+ runDoctor
78
+ };
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ var HELP = `elvix \u2014 @elvix.is/sdk CLI
5
+
6
+ Usage:
7
+ elvix mcp [--admin] [--base-url=<url>]
8
+ Launch the elvix MCP server on stdio. Reads ELVIX_API_KEY from
9
+ the environment. Read-only by default; --admin enables mutation
10
+ tools (the server still enforces the admin scope on the key).
11
+
12
+ elvix doctor [--client-id=<id>] [--base-url=<url>]
13
+ Diagnose an integration: base URL reachability, clientId
14
+ resolution, verify-endpoint liveness, API-key presence.
15
+ Reads ELVIX_CLIENT_ID / ELVIX_API_KEY / ELVIX_BASE_URL.
16
+
17
+ elvix help
18
+ Show this message.
19
+
20
+ Docs: https://elvix.is/docs/agents`;
21
+ async function main() {
22
+ const sub = process.argv[2];
23
+ switch (sub) {
24
+ case "mcp": {
25
+ const { createElvixMcpServer } = await import("../mcp/index.js");
26
+ const apiKey = process.env.ELVIX_API_KEY;
27
+ if (!apiKey) {
28
+ process.stderr.write("ELVIX_API_KEY environment variable is required.\n");
29
+ process.exit(1);
30
+ }
31
+ const admin = process.argv.includes("--admin");
32
+ const baseUrl = process.argv.find((a) => a.startsWith("--base-url="))?.split("=")[1];
33
+ const { connectStdio } = await createElvixMcpServer({ apiKey, readonly: !admin, baseUrl });
34
+ await connectStdio();
35
+ return;
36
+ }
37
+ case "doctor": {
38
+ const { runDoctor } = await import("./doctor.js");
39
+ process.exit(await runDoctor());
40
+ break;
41
+ }
42
+ case "help":
43
+ case "--help":
44
+ case "-h":
45
+ case void 0:
46
+ process.stdout.write(`${HELP}
47
+ `);
48
+ return;
49
+ default:
50
+ process.stderr.write(`Unknown command: ${sub}
51
+
52
+ ${HELP}
53
+ `);
54
+ process.exit(1);
55
+ }
56
+ }
57
+ main().catch((e) => {
58
+ process.stderr.write(`elvix: ${e instanceof Error ? e.message : String(e)}
59
+ `);
60
+ process.exit(1);
61
+ });
package/dist/mcp/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createElvixMcpServer
4
- } from "../chunk-22IQNPXM.js";
4
+ } from "../chunk-SSZNTXQK.js";
5
5
 
6
6
  // src/mcp/bin.ts
7
7
  async function main() {
package/dist/mcp/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createElvixMcpServer
3
- } from "../chunk-22IQNPXM.js";
3
+ } from "../chunk-SSZNTXQK.js";
4
4
  export {
5
5
  createElvixMcpServer
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvix.is/sdk",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Official elvix SDK. Drop-in React components, server helpers, and an MCP server so AI coding agents integrate elvix on the first try.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://elvix.is",
@@ -21,6 +21,7 @@
21
21
  "main": "./dist/index.js",
22
22
  "types": "./dist/index.d.ts",
23
23
  "bin": {
24
+ "elvix": "./dist/cli/index.js",
24
25
  "elvix-mcp": "./dist/mcp/bin.js"
25
26
  },
26
27
  "exports": {
@@ -47,7 +48,7 @@
47
48
  },
48
49
  "files": ["dist", "README.md", "LICENSE"],
49
50
  "scripts": {
50
- "build": "tsup src/index.ts src/react.ts src/server.ts src/types.ts src/mcp/index.ts src/mcp/bin.ts --format esm --dts --clean --external react --external next",
51
+ "build": "tsup src/index.ts src/react.ts src/server.ts src/types.ts src/mcp/index.ts src/mcp/bin.ts src/cli/index.ts src/cli/doctor.ts --format esm --dts --clean --external react --external next",
51
52
  "typecheck": "tsc --noEmit",
52
53
  "test": "vitest run"
53
54
  },