@aident-ai/cli 0.0.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -0
  3. package/dist/cli.mjs +1138 -0
  4. package/package.json +60 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aident AI, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @aident-ai/cli
2
+
3
+ The Aident command-line interface — agent-friendly access to 1000+ integrations, automation playbooks, and the full Aident platform.
4
+
5
+ ```bash
6
+ aident login
7
+ aident capabilities search --query "send email"
8
+ aident playbooks list --json
9
+ ```
10
+
11
+ This is the recommended way to use Aident from any coding agent (Claude Code, OpenClaw, HermesAgent, Cursor, …) or shell automation. The CLI is a thin wrapper around the Aident command engine — every command you run on the platform UI is available here, with the same semantics.
12
+
13
+ ## Install
14
+
15
+ ### One-line install (recommended)
16
+
17
+ **macOS / Linux:**
18
+
19
+ ```bash
20
+ curl -fsSL https://app.aident.ai/cli/install.sh | bash
21
+ ```
22
+
23
+ **Windows (PowerShell, with Git Bash installed):**
24
+
25
+ ```powershell
26
+ & "C:\Program Files\Git\bin\bash.exe" -c 'curl -fsSL https://app.aident.ai/cli/install.sh | bash'
27
+ ```
28
+
29
+ The installer requires Node ≥ 18. If you don't have it, install via [nodejs.org](https://nodejs.org) or a version manager (`brew install node`, `nvm install --lts`, `fnm install --lts`) and re-run.
30
+
31
+ ### Manual install
32
+
33
+ ```bash
34
+ # One-off (no install)
35
+ npx -y @aident-ai/cli <command>
36
+
37
+ # Global
38
+ npm install -g @aident-ai/cli
39
+ ```
40
+
41
+ ## Get started
42
+
43
+ ```bash
44
+ aident setup # interactive: pick base URL + log in
45
+ aident doctor # validate installation
46
+ aident --help # discover packages and domains
47
+ ```
48
+
49
+ The browser-based sign-in uses PKCE on a localhost loopback. If you can't bind a port (locked-down sandboxes, remote shells), pass `--oob`:
50
+
51
+ ```bash
52
+ aident login --oob
53
+ ```
54
+
55
+ That falls back to the OOB copy-paste flow.
56
+
57
+ ## Common commands
58
+
59
+ ```bash
60
+ aident capabilities --help # list commands in a domain
61
+ aident capabilities search --help # show input schema and examples
62
+ aident capabilities search --query "send email" # run a command (TUI output)
63
+ aident playbooks list --json # JSON output for piping
64
+ aident playbooks execute --playbookId pb_123 # run a playbook
65
+ aident integrations status # see connected integrations
66
+ aident logout # revoke and clear creds
67
+ ```
68
+
69
+ Use `--json` for machine-readable output. The CLI auto-detects whether stdout is a TTY.
70
+
71
+ ## Configuration
72
+
73
+ The CLI persists settings to `~/.aident/config.json` and credentials to `~/.aident/credentials.json` — the same `~/.aident/` directory used by the Aident skill, the desktop sandbox app, and any other Aident tooling.
74
+
75
+ ```bash
76
+ aident config show # print all config
77
+ aident config get baseUrl # print one value
78
+ aident config set baseUrl https://... # persist a value
79
+ aident config unset baseUrl # remove an override
80
+ aident config path # print the config file path
81
+ ```
82
+
83
+ | Key | Default | Purpose |
84
+ | --------- | ----------------------- | ---------------------------------------------------------- |
85
+ | `baseUrl` | `https://app.aident.ai` | API host used for new logins. Save once, reuse everywhere. |
86
+
87
+ ### Resolution order for the API base URL
88
+
89
+ 1. `AIDENT_BASE_URL` env var (per-invocation override)
90
+ 2. `aident login --base-url <url>` (only at login time)
91
+ 3. `~/.aident/config.json` `baseUrl`
92
+ 4. Active credentials file (after login carries its own host)
93
+ 5. Default `https://app.aident.ai`
94
+
95
+ ### Environment overrides
96
+
97
+ | Variable | Purpose |
98
+ | ----------------- | ------------------------------------------------------------------------- |
99
+ | `AIDENT_TOKEN` | Use this Bearer token directly; skips the credentials file. Useful in CI. |
100
+ | `AIDENT_BASE_URL` | Override the default server for a single invocation. |
101
+
102
+ ## Where files live
103
+
104
+ | Path | Owner | Purpose |
105
+ | ------------------------------------ | ------------------- | -------------------------------------------- |
106
+ | `~/.aident/config.json` | CLI (this package) | Persistent settings (`baseUrl`, …) |
107
+ | `~/.aident/credentials.json` | CLI + Aident skill | OAuth tokens for `aident` / agent skill REST |
108
+ | `~/.aident/sandbox-credentials.json` | Sandbox desktop app | Tokens used by the local sandbox runtime |
109
+ | `~/.aident/sandbox-workspaces.json` | Sandbox desktop app | Tracked workspace directories |
110
+ | `~/.aident/sandbox/` | Sandbox daemon | Daemon PID, status, logs |
111
+
112
+ All Aident tooling shares this directory; nothing escapes outside `~/.aident/`.
113
+
114
+ ## How it works
115
+
116
+ `aident <domain> <command> [args]` makes a `POST /api/cli/exec` request to the Aident server with `{ domain, command, args }`, the same shape used internally by the platform's CLI engine. Discovery (`--help`, schemas, examples) comes from `GET /api/cli`. The CLI is a thin layer — there is no Aident logic baked in. Anything you can do on the web app, you can do here.
117
+
118
+ ## License
119
+
120
+ MIT
package/dist/cli.mjs ADDED
@@ -0,0 +1,1138 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/auth.ts
4
+ import { spawn } from "node:child_process";
5
+ import { createHash, randomBytes } from "node:crypto";
6
+ import { createServer } from "node:http";
7
+
8
+ // src/credentials.ts
9
+ import { mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "node:fs/promises";
10
+ import { join as join2 } from "node:path";
11
+
12
+ // src/config.ts
13
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
14
+ import { homedir } from "node:os";
15
+ import { join } from "node:path";
16
+ function getAidentDir() {
17
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
18
+ return join(home, ".aident");
19
+ }
20
+ function getConfigFile() {
21
+ return join(getAidentDir(), "config.json");
22
+ }
23
+ var DEFAULT_BASE_URL = "https://app.aident.ai";
24
+ var CONFIG_KEYS = ["baseUrl"];
25
+ async function readConfig() {
26
+ try {
27
+ const text = await readFile(getConfigFile(), "utf-8");
28
+ const parsed = JSON.parse(text);
29
+ if (!parsed || typeof parsed !== "object")
30
+ return {};
31
+ return parsed;
32
+ } catch {
33
+ return {};
34
+ }
35
+ }
36
+ async function writeConfig(config) {
37
+ await mkdir(getAidentDir(), { recursive: true, mode: 448 });
38
+ await writeFile(getConfigFile(), JSON.stringify(config, null, 2) + `
39
+ `, { mode: 420 });
40
+ }
41
+ async function setConfigValue(key, value) {
42
+ const config = await readConfig();
43
+ config[key] = value;
44
+ await writeConfig(config);
45
+ }
46
+ async function unsetConfigValue(key) {
47
+ const config = await readConfig();
48
+ delete config[key];
49
+ await writeConfig(config);
50
+ }
51
+ async function resolveDefaultBaseUrl() {
52
+ const env = process.env.AIDENT_BASE_URL?.trim();
53
+ if (env)
54
+ return env;
55
+ const config = await readConfig();
56
+ if (typeof config.baseUrl === "string" && config.baseUrl.trim() !== "") {
57
+ return config.baseUrl.trim();
58
+ }
59
+ return DEFAULT_BASE_URL;
60
+ }
61
+ function isKnownConfigKey(key) {
62
+ return CONFIG_KEYS.includes(key);
63
+ }
64
+ function normalizeBaseUrl(url) {
65
+ let trimmed = url.trim();
66
+ if (!/^https?:\/\//i.test(trimmed))
67
+ trimmed = `https://${trimmed}`;
68
+ return trimmed.replace(/\/+$/, "");
69
+ }
70
+
71
+ // src/credentials.ts
72
+ function getCredentialsFile() {
73
+ return join2(getAidentDir(), "credentials.json");
74
+ }
75
+ async function readCredentials() {
76
+ try {
77
+ const content = await readFile2(getCredentialsFile(), "utf-8");
78
+ const parsed = JSON.parse(content);
79
+ if (!parsed.access_token || !parsed.base_url)
80
+ return null;
81
+ return parsed;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ async function writeCredentials(creds) {
87
+ await mkdir2(getAidentDir(), { recursive: true, mode: 448 });
88
+ await writeFile2(getCredentialsFile(), JSON.stringify(creds, null, 2), { mode: 384 });
89
+ }
90
+ async function clearCredentials() {
91
+ await rm(getCredentialsFile(), { force: true });
92
+ }
93
+ function isExpired(creds) {
94
+ if (!creds.expires_at)
95
+ return false;
96
+ const expiresAt = Date.parse(creds.expires_at);
97
+ if (Number.isNaN(expiresAt))
98
+ return false;
99
+ return Date.now() >= expiresAt - 60000;
100
+ }
101
+
102
+ // src/output.ts
103
+ var isTTY = process.stdout.isTTY === true;
104
+ var colors = {
105
+ reset: isTTY ? "\x1B[0m" : "",
106
+ red: isTTY ? "\x1B[31m" : "",
107
+ green: isTTY ? "\x1B[32m" : "",
108
+ yellow: isTTY ? "\x1B[33m" : "",
109
+ cyan: isTTY ? "\x1B[36m" : "",
110
+ dim: isTTY ? "\x1B[2m" : "",
111
+ bold: isTTY ? "\x1B[1m" : ""
112
+ };
113
+ function logInfo(text) {
114
+ process.stdout.write(text + `
115
+ `);
116
+ }
117
+ function logErr(text) {
118
+ process.stderr.write(text + `
119
+ `);
120
+ }
121
+
122
+ // src/prompt.ts
123
+ function readLine(prompt) {
124
+ return new Promise((resolve) => {
125
+ process.stdout.write(prompt);
126
+ let data = "";
127
+ const onData = (chunk) => {
128
+ const s = chunk.toString("utf-8");
129
+ const newlineIdx = s.indexOf(`
130
+ `);
131
+ if (newlineIdx === -1) {
132
+ data += s;
133
+ return;
134
+ }
135
+ data += s.slice(0, newlineIdx);
136
+ process.stdin.removeListener("data", onData);
137
+ process.stdin.pause();
138
+ resolve(data.replace(/\r$/, ""));
139
+ };
140
+ process.stdin.resume();
141
+ process.stdin.on("data", onData);
142
+ });
143
+ }
144
+
145
+ // src/auth.ts
146
+ var CLIENT_NAME = "aident-cli";
147
+ async function login(options) {
148
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
149
+ if (options.oob)
150
+ return loginOob(baseUrl);
151
+ try {
152
+ return await loginLoopback(baseUrl);
153
+ } catch (err) {
154
+ logErr(`Loopback OAuth failed (${err instanceof Error ? err.message : String(err)}). Falling back to OOB flow.`);
155
+ return loginOob(baseUrl);
156
+ }
157
+ }
158
+ async function refreshToken(creds) {
159
+ if (!creds.refresh_token || !creds.client_id)
160
+ return null;
161
+ const baseUrl = creds.base_url.replace(/\/+$/, "");
162
+ const body = new URLSearchParams({
163
+ grant_type: "refresh_token",
164
+ client_id: creds.client_id,
165
+ refresh_token: creds.refresh_token
166
+ });
167
+ const res = await fetch(`${baseUrl}/api/mcp/oauth/token`, {
168
+ method: "POST",
169
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
170
+ body
171
+ });
172
+ if (!res.ok)
173
+ return null;
174
+ const tok = await res.json();
175
+ return updateCredsFromToken(creds, tok);
176
+ }
177
+ async function logout(creds) {
178
+ const baseUrl = creds.base_url.replace(/\/+$/, "");
179
+ await fetch(`${baseUrl}/api/mcp/oauth/revoke`, {
180
+ method: "POST",
181
+ headers: {
182
+ "Content-Type": "application/x-www-form-urlencoded",
183
+ Authorization: `Bearer ${creds.access_token}`
184
+ },
185
+ body: new URLSearchParams({ token: creds.access_token })
186
+ }).catch(() => {
187
+ return;
188
+ });
189
+ }
190
+ async function loginLoopback(baseUrl) {
191
+ const verifier = base64UrlEncode(randomBytes(48));
192
+ const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
193
+ const state = base64UrlEncode(randomBytes(16));
194
+ const { server, port, codePromise } = await startCallbackServer(state);
195
+ const redirectUri = `http://localhost:${port}/callback`;
196
+ const clientId = await registerClient(baseUrl, [redirectUri]);
197
+ const authorizeUrl = new URL(`${baseUrl}/api/mcp/oauth/authorize`);
198
+ authorizeUrl.searchParams.set("response_type", "code");
199
+ authorizeUrl.searchParams.set("client_id", clientId);
200
+ authorizeUrl.searchParams.set("redirect_uri", redirectUri);
201
+ authorizeUrl.searchParams.set("code_challenge", challenge);
202
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
203
+ authorizeUrl.searchParams.set("state", state);
204
+ logInfo(`Opening browser for Aident login...`);
205
+ logInfo(`If the browser does not open, visit: ${authorizeUrl.toString()}`);
206
+ openBrowser(authorizeUrl.toString());
207
+ let code;
208
+ try {
209
+ code = await codePromise;
210
+ } finally {
211
+ server.close();
212
+ }
213
+ const tok = await exchangeCode(baseUrl, clientId, code, redirectUri, verifier);
214
+ return buildCreds(baseUrl, clientId, tok);
215
+ }
216
+ async function loginOob(baseUrl) {
217
+ const redirectUri = `${baseUrl}/mcp/oob`;
218
+ const verifier = base64UrlEncode(randomBytes(48));
219
+ const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
220
+ const state = base64UrlEncode(randomBytes(16));
221
+ const clientId = await reuseOrRegisterClient(baseUrl, [redirectUri]);
222
+ const authorizeUrl = new URL(`${baseUrl}/api/mcp/oauth/authorize`);
223
+ authorizeUrl.searchParams.set("response_type", "code");
224
+ authorizeUrl.searchParams.set("client_id", clientId);
225
+ authorizeUrl.searchParams.set("redirect_uri", redirectUri);
226
+ authorizeUrl.searchParams.set("code_challenge", challenge);
227
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
228
+ authorizeUrl.searchParams.set("state", state);
229
+ logInfo(`Opening browser for Aident login...`);
230
+ logInfo(`If the browser does not open, visit: ${authorizeUrl.toString()}`);
231
+ openBrowser(authorizeUrl.toString());
232
+ logInfo("After approving, paste the value shown on the Aident page below.");
233
+ logInfo("(It will be either an authorization code — preferred — or a raw access token.)");
234
+ const pasted = (await readLine("Paste here: ")).trim();
235
+ if (!pasted)
236
+ throw new Error("No token provided");
237
+ try {
238
+ const tok = await exchangeCode(baseUrl, clientId, pasted, redirectUri, verifier);
239
+ return buildCreds(baseUrl, clientId, tok);
240
+ } catch (err) {
241
+ if (err instanceof OAuthRejectedError) {
242
+ if (!looksLikeAccessToken(pasted)) {
243
+ throw new Error("Pasted value is neither a valid authorization code nor a recognizable access token. Please retry the login.");
244
+ }
245
+ logErr(`Server rejected the pasted value as an authorization code. Treating it as a raw access token. Note: token refresh is unavailable in this mode — when the token expires you'll need to run \`aident login\` again.`);
246
+ return { base_url: baseUrl, client_id: clientId, access_token: pasted };
247
+ }
248
+ throw err;
249
+ }
250
+ }
251
+ function looksLikeAccessToken(value) {
252
+ if (value.length < 20)
253
+ return false;
254
+ if (/\s/.test(value))
255
+ return false;
256
+ return /^[A-Za-z0-9._-]+$/.test(value);
257
+ }
258
+ async function reuseOrRegisterClient(baseUrl, redirectUris) {
259
+ const existing = await readCredentials();
260
+ if (existing && existing.client_id && existing.base_url === baseUrl)
261
+ return existing.client_id;
262
+ return registerClient(baseUrl, redirectUris);
263
+ }
264
+ async function registerClient(baseUrl, redirectUris) {
265
+ const res = await fetch(`${baseUrl}/api/mcp/oauth/register`, {
266
+ method: "POST",
267
+ headers: { "Content-Type": "application/json" },
268
+ body: JSON.stringify({
269
+ client_name: CLIENT_NAME,
270
+ redirect_uris: redirectUris,
271
+ grant_types: ["authorization_code", "refresh_token"],
272
+ response_types: ["code"],
273
+ token_endpoint_auth_method: "none"
274
+ })
275
+ });
276
+ if (!res.ok) {
277
+ const text = await res.text().catch(() => "");
278
+ throw new Error(`Failed to register OAuth client (HTTP ${res.status}): ${text}`);
279
+ }
280
+ const json = await res.json();
281
+ if (!json.client_id)
282
+ throw new Error("OAuth registration response missing client_id");
283
+ return json.client_id;
284
+ }
285
+
286
+ class OAuthRejectedError extends Error {
287
+ status;
288
+ constructor(message, status) {
289
+ super(message);
290
+ this.status = status;
291
+ this.name = "OAuthRejectedError";
292
+ }
293
+ }
294
+ async function exchangeCode(baseUrl, clientId, code, redirectUri, verifier) {
295
+ const body = new URLSearchParams({
296
+ grant_type: "authorization_code",
297
+ client_id: clientId,
298
+ code,
299
+ redirect_uri: redirectUri,
300
+ code_verifier: verifier
301
+ });
302
+ const res = await fetch(`${baseUrl}/api/mcp/oauth/token`, {
303
+ method: "POST",
304
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
305
+ body
306
+ });
307
+ if (!res.ok) {
308
+ const text = await res.text().catch(() => "");
309
+ if (res.status >= 400 && res.status < 500) {
310
+ throw new OAuthRejectedError(`Token exchange rejected (HTTP ${res.status}): ${text}`, res.status);
311
+ }
312
+ throw new Error(`Token exchange failed (HTTP ${res.status}): ${text}`);
313
+ }
314
+ return await res.json();
315
+ }
316
+ function buildCreds(baseUrl, clientId, tok) {
317
+ const expiresAt = tok.expires_in ? new Date(Date.now() + tok.expires_in * 1000).toISOString() : undefined;
318
+ return {
319
+ base_url: baseUrl,
320
+ client_id: clientId,
321
+ access_token: tok.access_token,
322
+ refresh_token: tok.refresh_token,
323
+ expires_at: expiresAt
324
+ };
325
+ }
326
+ function updateCredsFromToken(creds, tok) {
327
+ const expiresAt = tok.expires_in ? new Date(Date.now() + tok.expires_in * 1000).toISOString() : creds.expires_at;
328
+ return {
329
+ ...creds,
330
+ access_token: tok.access_token,
331
+ refresh_token: tok.refresh_token ?? creds.refresh_token,
332
+ expires_at: expiresAt
333
+ };
334
+ }
335
+ var LOOPBACK_TIMEOUT_MS = 5 * 60 * 1000;
336
+ function startCallbackServer(expectedState) {
337
+ return new Promise((resolve, reject) => {
338
+ let codeResolver = () => {
339
+ return;
340
+ };
341
+ let codeRejecter = () => {
342
+ return;
343
+ };
344
+ const codePromise = new Promise((res, rej) => {
345
+ codeResolver = res;
346
+ codeRejecter = rej;
347
+ });
348
+ const timeout = setTimeout(() => {
349
+ codeRejecter(new Error(`No callback received within ${LOOPBACK_TIMEOUT_MS / 1000}s; aborting.`));
350
+ }, LOOPBACK_TIMEOUT_MS);
351
+ codePromise.finally(() => clearTimeout(timeout));
352
+ const server = createServer((req, res) => {
353
+ if (!req.url) {
354
+ res.writeHead(400);
355
+ res.end("Bad request");
356
+ return;
357
+ }
358
+ const url = new URL(req.url, `http://localhost`);
359
+ if (url.pathname !== "/callback") {
360
+ res.writeHead(404);
361
+ res.end("Not found");
362
+ return;
363
+ }
364
+ const code = url.searchParams.get("code");
365
+ const state = url.searchParams.get("state");
366
+ const error = url.searchParams.get("error");
367
+ if (error) {
368
+ res.writeHead(400, { "Content-Type": "text/html" });
369
+ res.end(`<html><body><h2>Authentication error</h2><p>${escapeHtml(error)}</p></body></html>`);
370
+ codeRejecter(new Error(`OAuth error: ${error}`));
371
+ return;
372
+ }
373
+ if (!code || state !== expectedState) {
374
+ res.writeHead(400, { "Content-Type": "text/html" });
375
+ res.end("<html><body><h2>Invalid callback</h2></body></html>");
376
+ codeRejecter(new Error("Invalid callback (missing code or bad state)"));
377
+ return;
378
+ }
379
+ res.writeHead(200, { "Content-Type": "text/html" });
380
+ res.end(`<html><body style="font-family:system-ui;padding:40px;text-align:center"><h2>You're signed in.</h2><p>You can close this window and return to the terminal.</p></body></html>`);
381
+ codeResolver(code);
382
+ });
383
+ server.once("error", reject);
384
+ server.listen(0, "127.0.0.1", () => {
385
+ const addr = server.address();
386
+ resolve({ server, port: addr.port, codePromise });
387
+ });
388
+ });
389
+ }
390
+ function openBrowser(url) {
391
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
392
+ const args = process.platform === "win32" ? ["", url] : [url];
393
+ try {
394
+ spawn(cmd, args, { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
395
+ } catch {}
396
+ }
397
+ function base64UrlEncode(buf) {
398
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
399
+ }
400
+ function escapeHtml(s) {
401
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
402
+ }
403
+
404
+ // src/version.ts
405
+ var VERSION = "0.0.0";
406
+
407
+ // src/client.ts
408
+ class CliClient {
409
+ creds;
410
+ constructor(creds) {
411
+ this.creds = creds;
412
+ }
413
+ get baseUrl() {
414
+ return this.creds.base_url.replace(/\/+$/, "");
415
+ }
416
+ updateCredentials(creds) {
417
+ this.creds = creds;
418
+ }
419
+ async getCatalog() {
420
+ return this.fetchJson("GET", "/api/cli");
421
+ }
422
+ async exec(domain, command, args) {
423
+ return this.fetchJson("POST", "/api/cli/exec", { domain, command, args });
424
+ }
425
+ async fetchJson(method, path, body) {
426
+ const headers = {
427
+ Authorization: `Bearer ${this.creds.access_token}`,
428
+ "User-Agent": `@aident/cli/${VERSION}`
429
+ };
430
+ if (body !== undefined)
431
+ headers["Content-Type"] = "application/json";
432
+ const res = await fetch(`${this.baseUrl}${path}`, {
433
+ method,
434
+ headers,
435
+ body: body === undefined ? undefined : JSON.stringify(body)
436
+ });
437
+ const text = await res.text();
438
+ try {
439
+ return { status: res.status, body: JSON.parse(text) };
440
+ } catch {
441
+ return { status: res.status, body: invalidResponseFallback(text) };
442
+ }
443
+ }
444
+ }
445
+ function invalidResponseFallback(text) {
446
+ return { success: false, error: { code: "invalid-response", message: text } };
447
+ }
448
+
449
+ // src/doctor.ts
450
+ async function runDoctor(opts) {
451
+ const checks = [];
452
+ checks.push(checkNodeVersion());
453
+ checks.push(await checkConfig());
454
+ checks.push(await checkCredentials(opts.baseUrl));
455
+ checks.push(await checkServerReachable(opts.baseUrl));
456
+ const ok = checks.every((c) => c.ok);
457
+ return { ok, checks, ...opts };
458
+ }
459
+ function checkNodeVersion() {
460
+ const version = process.versions.node;
461
+ const major = Number(version.split(".")[0]);
462
+ const ok = Number.isFinite(major) && major >= 18;
463
+ return {
464
+ name: "Node.js >= 18",
465
+ ok,
466
+ detail: ok ? `node v${version}` : `node v${version} (need >= 18)`
467
+ };
468
+ }
469
+ async function checkConfig() {
470
+ const config = await readConfig();
471
+ const hasBaseUrl = typeof config.baseUrl === "string" && config.baseUrl.length > 0;
472
+ return {
473
+ name: "Config file",
474
+ ok: true,
475
+ detail: hasBaseUrl ? `baseUrl=${config.baseUrl}` : "no overrides (using defaults)"
476
+ };
477
+ }
478
+ async function checkCredentials(baseUrl) {
479
+ if (process.env.AIDENT_TOKEN) {
480
+ return { name: "Authenticated", ok: true, detail: "using AIDENT_TOKEN env var" };
481
+ }
482
+ const creds = await readCredentials();
483
+ if (!creds) {
484
+ return { name: "Authenticated", ok: false, detail: "no credentials — run `aident login`" };
485
+ }
486
+ if (creds.base_url !== baseUrl) {
487
+ return {
488
+ name: "Authenticated",
489
+ ok: false,
490
+ detail: `credentials are for ${creds.base_url} but config baseUrl is ${baseUrl} — run \`aident login\``
491
+ };
492
+ }
493
+ return { name: "Authenticated", ok: true, detail: `signed in to ${creds.base_url}` };
494
+ }
495
+ async function checkServerReachable(baseUrl) {
496
+ try {
497
+ const res = await fetch(`${baseUrl}/api/cli`, {
498
+ method: "GET",
499
+ headers: { "User-Agent": `@aident/cli/${VERSION}` }
500
+ });
501
+ if (res.status === 426) {
502
+ const minVersion = res.headers.get("x-aident-min-cli-version") ?? "newer";
503
+ return {
504
+ name: "Server reachable",
505
+ ok: false,
506
+ detail: `${baseUrl} requires @aident/cli ≥ ${minVersion} — run \`npm install -g @aident/cli@latest\``
507
+ };
508
+ }
509
+ if (res.status === 401 || res.status === 200) {
510
+ return { name: "Server reachable", ok: true, detail: `${baseUrl} responded ${res.status}` };
511
+ }
512
+ return { name: "Server reachable", ok: false, detail: `${baseUrl} responded ${res.status}` };
513
+ } catch (err) {
514
+ return {
515
+ name: "Server reachable",
516
+ ok: false,
517
+ detail: `failed to reach ${baseUrl}: ${err instanceof Error ? err.message : String(err)}`
518
+ };
519
+ }
520
+ }
521
+
522
+ // src/help.ts
523
+ function renderHelp(catalog) {
524
+ const lines = [];
525
+ lines.push(`${colors.bold}AIDENT — Platform Feature CLI${colors.reset}`);
526
+ lines.push("");
527
+ lines.push("USAGE:");
528
+ lines.push(" aident <domain> <command> [--flag value ...] [--json]");
529
+ lines.push(" aident login Authenticate with Aident");
530
+ lines.push(" aident logout Revoke the current token");
531
+ lines.push(" aident whoami Show current user");
532
+ lines.push(" aident <domain> help List commands in a domain");
533
+ lines.push(" aident <domain> <command> help Show input schema and examples");
534
+ lines.push("");
535
+ if (catalog.packages.length > 0) {
536
+ lines.push("PACKAGES:");
537
+ for (const pkg of catalog.packages) {
538
+ lines.push(` ${pkg.name.padEnd(10)} ${pkg.description}`);
539
+ }
540
+ lines.push("");
541
+ }
542
+ lines.push("DOMAINS:");
543
+ for (const d of catalog.domains) {
544
+ const display = d.isAdmin ? `admin ${d.name.replace("admin:", "")}` : d.name;
545
+ lines.push(` ${display.padEnd(28)} ${d.description} ${colors.dim}(${d.commandCount})${colors.reset}`);
546
+ }
547
+ return lines.join(`
548
+ `);
549
+ }
550
+ function renderDomainHelp(catalog, domain) {
551
+ const cmds = catalog.commands.filter((c) => c.domain === domain);
552
+ if (cmds.length === 0)
553
+ return `Unknown domain: ${domain}`;
554
+ const dInfo = catalog.domains.find((d) => d.name === domain);
555
+ const display = domain.startsWith("admin:") ? `admin ${domain.replace("admin:", "")}` : domain;
556
+ const lines = [];
557
+ lines.push(`${colors.bold}AIDENT ${display.toUpperCase()}${colors.reset}`);
558
+ if (dInfo?.description) {
559
+ lines.push("");
560
+ lines.push(dInfo.description);
561
+ }
562
+ lines.push("");
563
+ lines.push("COMMANDS:");
564
+ for (const c of cmds) {
565
+ lines.push(` ${c.command.padEnd(28)} ${c.description}`);
566
+ }
567
+ lines.push("");
568
+ lines.push(`Run "aident ${display} <command> help" for input schema and examples.`);
569
+ return lines.join(`
570
+ `);
571
+ }
572
+ function renderCommandHelp(catalog, domain, command) {
573
+ const cmd = catalog.commands.find((c) => c.domain === domain && c.command === command);
574
+ if (!cmd)
575
+ return `Unknown command: ${domain} ${command}`;
576
+ const display = domain.startsWith("admin:") ? `admin ${domain.replace("admin:", "")}` : domain;
577
+ const lines = [];
578
+ lines.push(`${colors.bold}AIDENT ${display.toUpperCase()} ${command.toUpperCase()}${colors.reset}`);
579
+ lines.push("");
580
+ lines.push(cmd.description);
581
+ if (cmd.longDescription) {
582
+ lines.push("");
583
+ lines.push(cmd.longDescription);
584
+ }
585
+ const schema = cmd.inputSchema;
586
+ if (schema?.properties) {
587
+ const required = new Set(schema.required ?? []);
588
+ lines.push("");
589
+ lines.push("INPUT:");
590
+ for (const [key, val] of Object.entries(schema.properties)) {
591
+ const tag = required.has(key) ? "" : " (optional)";
592
+ const type = val.type ? ` ${colors.dim}<${val.type}>${colors.reset}` : "";
593
+ lines.push(` --${key.padEnd(24)}${type} ${val.description ?? ""}${tag}`);
594
+ }
595
+ }
596
+ if (cmd.outputDescription) {
597
+ lines.push("");
598
+ lines.push(`OUTPUT: ${cmd.outputDescription}`);
599
+ }
600
+ if (cmd.examples && cmd.examples.length > 0) {
601
+ lines.push("");
602
+ lines.push("EXAMPLES:");
603
+ for (const ex of cmd.examples) {
604
+ lines.push(` ${colors.dim}# ${ex.description}${colors.reset}`);
605
+ const argStr = Object.entries(ex.args).map(([k, v]) => `--${k} ${typeof v === "string" ? `"${v}"` : JSON.stringify(v)}`).join(" ");
606
+ lines.push(` aident ${display} ${command} ${argStr}`);
607
+ lines.push("");
608
+ }
609
+ }
610
+ return lines.join(`
611
+ `);
612
+ }
613
+
614
+ // src/parser.ts
615
+ var RESERVED_CLI_FLAGS = ["base-url", "json", "tui", "help", "oob", "version"];
616
+ function parseArgs(argv) {
617
+ const positional = [];
618
+ const flags = {};
619
+ let isHelp = false;
620
+ let format = process.stdout.isTTY === true ? "tui" : "json";
621
+ let i = 0;
622
+ while (i < argv.length) {
623
+ const arg = argv[i];
624
+ if (arg === undefined) {
625
+ i++;
626
+ continue;
627
+ }
628
+ if (arg === "--help" || arg === "-h" || arg === "help") {
629
+ isHelp = true;
630
+ i++;
631
+ continue;
632
+ }
633
+ if (arg === "--json") {
634
+ format = "json";
635
+ i++;
636
+ continue;
637
+ }
638
+ if (arg === "--tui") {
639
+ format = "tui";
640
+ i++;
641
+ continue;
642
+ }
643
+ if (arg.startsWith("--")) {
644
+ const key = arg.slice(2);
645
+ const next = argv[i + 1];
646
+ if (next === undefined || next.startsWith("--")) {
647
+ flags[key] = true;
648
+ i++;
649
+ } else {
650
+ flags[key] = coerce(next);
651
+ i += 2;
652
+ }
653
+ continue;
654
+ }
655
+ positional.push(arg);
656
+ i++;
657
+ }
658
+ return { positional, flags, format, isHelp, raw: argv };
659
+ }
660
+ function coerce(val) {
661
+ if (val === "true")
662
+ return true;
663
+ if (val === "false")
664
+ return false;
665
+ if (val === "null")
666
+ return null;
667
+ if (val.startsWith("{") || val.startsWith("[")) {
668
+ try {
669
+ return JSON.parse(val);
670
+ } catch {}
671
+ }
672
+ if (val.trim() !== "" && /^-?\d+(\.\d+)?$/.test(val)) {
673
+ const n = Number(val);
674
+ if (Number.isFinite(n))
675
+ return n;
676
+ }
677
+ return val;
678
+ }
679
+ function resolveCommand(positional, knownCommands) {
680
+ if (positional.length === 0)
681
+ return null;
682
+ let domain = positional[0];
683
+ let consumed = 1;
684
+ if (domain === "admin" && positional.length >= 2) {
685
+ domain = `admin:${positional[1]}`;
686
+ consumed = 2;
687
+ }
688
+ const remaining = positional.slice(consumed);
689
+ if (remaining.length === 0)
690
+ return { domain, command: "", remaining: [] };
691
+ const domainCommands = knownCommands.filter((c) => c.domain === domain);
692
+ for (let len = Math.min(remaining.length, 5);len >= 1; len--) {
693
+ const candidate = remaining.slice(0, len).join(" ");
694
+ if (domainCommands.some((c) => c.command === candidate)) {
695
+ return { domain, command: candidate, remaining: remaining.slice(len) };
696
+ }
697
+ }
698
+ return { domain, command: remaining[0], remaining: remaining.slice(1) };
699
+ }
700
+
701
+ // src/refresh.ts
702
+ var defaultDeps = {
703
+ readCredentials,
704
+ writeCredentials,
705
+ refreshToken,
706
+ onUpgradeRequired: (message) => {
707
+ logErr(`${colors.red}✗ upgrade-required:${colors.reset} ${message}`);
708
+ process.exitCode = 1;
709
+ }
710
+ };
711
+ async function callWithRefresh(client, call, deps = defaultDeps) {
712
+ const first = await call(client);
713
+ if (first.status === 426) {
714
+ deps.onUpgradeRequired(extractUpgradeMessage(first.body));
715
+ return first;
716
+ }
717
+ if (first.status !== 401)
718
+ return first;
719
+ const creds = await deps.readCredentials();
720
+ if (!creds)
721
+ return first;
722
+ const refreshed = await deps.refreshToken(creds);
723
+ if (!refreshed)
724
+ return first;
725
+ await deps.writeCredentials(refreshed);
726
+ client.updateCredentials(refreshed);
727
+ const retried = await call(client);
728
+ if (retried.status === 426) {
729
+ deps.onUpgradeRequired(extractUpgradeMessage(retried.body));
730
+ }
731
+ return retried;
732
+ }
733
+ function extractUpgradeMessage(body) {
734
+ const err = body?.error;
735
+ return err?.message ?? `This @aident/cli version is no longer supported. Run \`npm install -g @aident/cli@latest\` to upgrade.`;
736
+ }
737
+
738
+ // src/cli.ts
739
+ async function main() {
740
+ const argv = process.argv.slice(2);
741
+ const parsed = parseArgs(argv);
742
+ const first = parsed.positional[0];
743
+ if (parsed.flags["version"] === true || first === "version") {
744
+ logInfo(`@aident/cli v${VERSION}`);
745
+ return;
746
+ }
747
+ if (!first || parsed.isHelp && parsed.positional.length === 0) {
748
+ await runHelp(parsed.format);
749
+ return;
750
+ }
751
+ switch (first) {
752
+ case "login":
753
+ await runLogin(parsed);
754
+ return;
755
+ case "logout":
756
+ await runLogout();
757
+ return;
758
+ case "whoami":
759
+ await runWhoami(parsed.format);
760
+ return;
761
+ case "doctor":
762
+ await runDoctorCmd(parsed.format);
763
+ return;
764
+ case "setup":
765
+ await runSetup();
766
+ return;
767
+ case "config":
768
+ await runConfigCmd(parsed);
769
+ return;
770
+ case "catalog":
771
+ await runCatalog(parsed.format);
772
+ return;
773
+ default:
774
+ await runCommand(parsed);
775
+ }
776
+ }
777
+ async function runHelp(format) {
778
+ const client = await getAuthenticatedClient();
779
+ if (!client) {
780
+ if (format === "json") {
781
+ logInfo(JSON.stringify({ error: "not-authenticated", message: "Run `aident login` first." }));
782
+ } else {
783
+ logInfo(`${colors.bold}Aident CLI v${VERSION}${colors.reset}`);
784
+ logInfo("");
785
+ logInfo("Run `aident login` to authenticate, then `aident --help` to list commands.");
786
+ logInfo("");
787
+ logInfo("USAGE:");
788
+ logInfo(" aident login [--oob] [--base-url <url>] Authenticate with Aident");
789
+ logInfo(" aident logout Revoke the current token");
790
+ logInfo(" aident whoami Show current user");
791
+ logInfo(" aident config show Print persistent config");
792
+ logInfo(" aident config set <key> <value> Persist a config value");
793
+ logInfo(" aident config get <key> Read a single value");
794
+ logInfo(" aident doctor Validate installation");
795
+ logInfo(" aident setup Interactive setup wizard");
796
+ logInfo(" aident <domain> <command> [--flag value ...] [--json]");
797
+ }
798
+ return;
799
+ }
800
+ const catalog = await fetchCatalog(client);
801
+ if (!catalog)
802
+ return;
803
+ if (format === "json") {
804
+ logInfo(JSON.stringify(catalog));
805
+ } else {
806
+ logInfo(renderHelp(catalog));
807
+ }
808
+ }
809
+ async function runLogin(parsed) {
810
+ const flagBaseUrlRaw = parsed.flags["base-url"];
811
+ const flagBaseUrl = typeof flagBaseUrlRaw === "string" ? flagBaseUrlRaw : undefined;
812
+ const baseUrl = flagBaseUrl ? normalizeBaseUrl(flagBaseUrl) : await resolveDefaultBaseUrl();
813
+ const oob = parsed.flags["oob"] === true;
814
+ const creds = await login({ baseUrl, oob });
815
+ await writeCredentials(creds);
816
+ logInfo(`${colors.green}Signed in to ${creds.base_url}${colors.reset}`);
817
+ await setConfigValue("baseUrl", creds.base_url);
818
+ }
819
+ async function runLogout() {
820
+ const creds = await readCredentials();
821
+ if (!creds) {
822
+ logInfo("Not signed in.");
823
+ return;
824
+ }
825
+ await logout(creds);
826
+ await clearCredentials();
827
+ logInfo(`${colors.green}Signed out.${colors.reset}`);
828
+ }
829
+ async function runWhoami(format) {
830
+ const client = await getAuthenticatedClient();
831
+ if (!client) {
832
+ if (format === "json") {
833
+ logInfo(JSON.stringify({ authenticated: false }));
834
+ } else {
835
+ logInfo("Not signed in. Run `aident login`.");
836
+ }
837
+ process.exitCode = 1;
838
+ return;
839
+ }
840
+ const result = await callWithRefresh(client, (c) => c.exec("user", "me", {}));
841
+ if (format === "json") {
842
+ logInfo(JSON.stringify(result.body));
843
+ if (!result.body.success)
844
+ process.exitCode = 1;
845
+ return;
846
+ }
847
+ if (result.body.success && result.body.data) {
848
+ const data = result.body.data;
849
+ logInfo(`${colors.green}Signed in${colors.reset} as ${data.email ?? data.userId ?? "unknown"}`);
850
+ return;
851
+ }
852
+ const err = result.body.error;
853
+ logErr(`${colors.red}✗ ${err?.code ?? "error"}:${colors.reset} ${err?.message ?? "Unable to fetch user info."}`);
854
+ process.exitCode = 1;
855
+ }
856
+ async function runConfigCmd(parsed) {
857
+ const sub = parsed.positional[1];
858
+ if (!sub || sub === "show" || sub === "list") {
859
+ const config = await readConfig();
860
+ if (parsed.format === "json") {
861
+ logInfo(JSON.stringify({ ...config, configFile: getConfigFile() }));
862
+ return;
863
+ }
864
+ logInfo(`${colors.bold}Aident config${colors.reset} ${colors.dim}(${getConfigFile()})${colors.reset}`);
865
+ if (Object.keys(config).length === 0) {
866
+ logInfo(` ${colors.dim}(empty — using defaults)${colors.reset}`);
867
+ logInfo(` ${colors.dim}baseUrl${colors.reset} ${colors.dim}default: ${DEFAULT_BASE_URL}${colors.reset}`);
868
+ return;
869
+ }
870
+ for (const [key, val] of Object.entries(config)) {
871
+ logInfo(` ${key.padEnd(15)} ${typeof val === "string" ? val : JSON.stringify(val)}`);
872
+ }
873
+ return;
874
+ }
875
+ if (sub === "path") {
876
+ logInfo(getConfigFile());
877
+ return;
878
+ }
879
+ if (sub === "get") {
880
+ const key = parsed.positional[2];
881
+ if (!key) {
882
+ logErr("Usage: aident config get <key>");
883
+ process.exitCode = 1;
884
+ return;
885
+ }
886
+ const config = await readConfig();
887
+ const val = config[key];
888
+ if (parsed.format === "json") {
889
+ logInfo(JSON.stringify(val ?? null));
890
+ } else if (val === undefined) {
891
+ logErr(`Not set: ${key}`);
892
+ process.exitCode = 1;
893
+ } else {
894
+ logInfo(typeof val === "string" ? val : JSON.stringify(val));
895
+ }
896
+ return;
897
+ }
898
+ if (sub === "set") {
899
+ const key = parsed.positional[2];
900
+ const rawValue = parsed.positional[3];
901
+ if (!key || rawValue === undefined) {
902
+ logErr("Usage: aident config set <key> <value>");
903
+ logErr(`Known keys: ${CONFIG_KEYS.join(", ")}`);
904
+ process.exitCode = 1;
905
+ return;
906
+ }
907
+ if (!isKnownConfigKey(key)) {
908
+ logErr(`Unknown config key: ${key}`);
909
+ logErr(`Known keys: ${CONFIG_KEYS.join(", ")}`);
910
+ process.exitCode = 1;
911
+ return;
912
+ }
913
+ const value = key === "baseUrl" ? normalizeBaseUrl(rawValue) : rawValue;
914
+ await setConfigValue(key, value);
915
+ logInfo(`${colors.green}Set${colors.reset} ${key} = ${value}`);
916
+ if (key === "baseUrl") {
917
+ const creds = await readCredentials();
918
+ if (creds && creds.base_url !== value) {
919
+ logInfo(`${colors.yellow}Note:${colors.reset} existing credentials are for ${creds.base_url}. Run \`aident login\` to authenticate against ${value}.`);
920
+ }
921
+ }
922
+ return;
923
+ }
924
+ if (sub === "unset") {
925
+ const key = parsed.positional[2];
926
+ if (!key) {
927
+ logErr("Usage: aident config unset <key>");
928
+ process.exitCode = 1;
929
+ return;
930
+ }
931
+ await unsetConfigValue(key);
932
+ logInfo(`${colors.green}Unset${colors.reset} ${key}`);
933
+ return;
934
+ }
935
+ logErr(`Unknown config subcommand: ${sub}`);
936
+ logErr("Usage: aident config [show | get <key> | set <key> <value> | unset <key> | path]");
937
+ process.exitCode = 1;
938
+ }
939
+ async function runDoctorCmd(format) {
940
+ const baseUrl = await resolveDefaultBaseUrl();
941
+ const report = await runDoctor({ baseUrl, configFile: getConfigFile(), credentialsFile: getCredentialsFile() });
942
+ if (format === "json") {
943
+ logInfo(JSON.stringify(report));
944
+ if (!report.ok)
945
+ process.exitCode = 1;
946
+ return;
947
+ }
948
+ logInfo(`${colors.bold}Aident CLI doctor${colors.reset}`);
949
+ logInfo("");
950
+ for (const c of report.checks) {
951
+ const mark = c.ok ? `${colors.green}✓${colors.reset}` : `${colors.red}✗${colors.reset}`;
952
+ logInfo(` ${mark} ${c.name.padEnd(22)} ${colors.dim}${c.detail}${colors.reset}`);
953
+ }
954
+ logInfo("");
955
+ logInfo(` baseUrl: ${baseUrl}`);
956
+ logInfo(` config: ${getConfigFile()}`);
957
+ logInfo(` credentials: ${getCredentialsFile()}`);
958
+ if (!report.ok)
959
+ process.exitCode = 1;
960
+ }
961
+ async function runSetup() {
962
+ logInfo(`${colors.bold}Aident CLI setup${colors.reset}`);
963
+ logInfo("");
964
+ const current = await resolveDefaultBaseUrl();
965
+ const input = (await readLine(`Base URL [${current}]: `)).trim();
966
+ const chosen = input ? normalizeBaseUrl(input) : current;
967
+ if (chosen !== current) {
968
+ await setConfigValue("baseUrl", chosen);
969
+ logInfo(`${colors.green}Saved${colors.reset} baseUrl = ${chosen}`);
970
+ }
971
+ logInfo("");
972
+ const existing = await readCredentials();
973
+ if (existing && existing.base_url === chosen) {
974
+ logInfo(`${colors.green}Already signed in${colors.reset} to ${chosen}.`);
975
+ } else {
976
+ const proceed = (await readLine(`Open browser to authenticate now? [Y/n]: `)).trim().toLowerCase();
977
+ if (proceed === "" || proceed === "y" || proceed === "yes") {
978
+ const creds = await login({ baseUrl: chosen });
979
+ await writeCredentials(creds);
980
+ logInfo(`${colors.green}Signed in to ${creds.base_url}${colors.reset}`);
981
+ } else {
982
+ logInfo("Skipped login. Run `aident login` whenever you want.");
983
+ }
984
+ }
985
+ logInfo("");
986
+ logInfo(`Done. Try ${colors.cyan}aident --help${colors.reset} or ${colors.cyan}aident doctor${colors.reset}.`);
987
+ }
988
+ async function runCatalog(format) {
989
+ const client = await getAuthenticatedClient();
990
+ if (!client) {
991
+ process.exitCode = 1;
992
+ return;
993
+ }
994
+ const catalog = await fetchCatalog(client);
995
+ if (!catalog)
996
+ return;
997
+ logInfo(JSON.stringify(catalog, null, format === "json" ? 0 : 2));
998
+ }
999
+ async function runCommand(parsed) {
1000
+ const client = await getAuthenticatedClient();
1001
+ if (!client) {
1002
+ if (parsed.format === "json") {
1003
+ logInfo(JSON.stringify({ success: false, error: { code: "not-authenticated", message: "Run `aident login` first." } }));
1004
+ } else {
1005
+ logErr("Not signed in. Run `aident login` first.");
1006
+ }
1007
+ process.exitCode = 1;
1008
+ return;
1009
+ }
1010
+ const catalog = await fetchCatalog(client);
1011
+ if (!catalog)
1012
+ return;
1013
+ const resolved = resolveCommand(parsed.positional, catalog.commands);
1014
+ if (!resolved?.command) {
1015
+ if (resolved && (parsed.isHelp || parsed.format === "tui")) {
1016
+ logInfo(renderDomainHelp(catalog, resolved.domain));
1017
+ return;
1018
+ }
1019
+ if (parsed.format === "json") {
1020
+ logInfo(JSON.stringify({ success: false, error: { code: "unknown-command", message: "Unknown command" } }));
1021
+ } else {
1022
+ logErr("Unknown command. Run `aident --help` to see available commands.");
1023
+ }
1024
+ process.exitCode = 1;
1025
+ return;
1026
+ }
1027
+ const cmdInfo = catalog.commands.find((c) => c.domain === resolved.domain && c.command === resolved.command);
1028
+ if (!cmdInfo) {
1029
+ if (parsed.format === "json") {
1030
+ logInfo(JSON.stringify({
1031
+ success: false,
1032
+ error: { code: "unknown-command", message: `Unknown command: ${resolved.domain} ${resolved.command}` }
1033
+ }));
1034
+ } else {
1035
+ logErr(`Unknown command: ${resolved.domain} ${resolved.command}`);
1036
+ logErr(`Run \`aident ${resolved.domain} --help\` to see available commands in this domain.`);
1037
+ }
1038
+ process.exitCode = 1;
1039
+ return;
1040
+ }
1041
+ if (parsed.isHelp) {
1042
+ logInfo(renderCommandHelp(catalog, resolved.domain, resolved.command));
1043
+ return;
1044
+ }
1045
+ const args = { ...parsed.flags };
1046
+ for (const reserved of RESERVED_CLI_FLAGS) {
1047
+ delete args[reserved];
1048
+ }
1049
+ if (resolved.remaining.length > 0) {
1050
+ const schema = cmdInfo.inputSchema;
1051
+ const required = new Set(schema?.required ?? []);
1052
+ const props = schema?.properties ?? {};
1053
+ for (const key of Object.keys(props)) {
1054
+ if (resolved.remaining.length === 0)
1055
+ break;
1056
+ if (!required.has(key))
1057
+ continue;
1058
+ if (key in args)
1059
+ continue;
1060
+ args[key] = resolved.remaining.shift();
1061
+ }
1062
+ }
1063
+ const result = await callWithRefresh(client, (c) => c.exec(resolved.domain, resolved.command, args));
1064
+ emitResult(result, parsed.format, resolved);
1065
+ }
1066
+ function emitResult(result, format, resolved) {
1067
+ const body = result.body;
1068
+ if (format === "json") {
1069
+ logInfo(JSON.stringify(body));
1070
+ if (!body.success)
1071
+ process.exitCode = 1;
1072
+ return;
1073
+ }
1074
+ if (body.success) {
1075
+ logInfo(`${colors.green}✓${colors.reset} ${resolved.domain} ${resolved.command}`);
1076
+ if (body.data !== undefined)
1077
+ logInfo(JSON.stringify(body.data, null, 2));
1078
+ if (body.meta)
1079
+ logInfo(`${colors.cyan}meta:${colors.reset} ${JSON.stringify(body.meta)}`);
1080
+ } else {
1081
+ logErr(`${colors.red}✗ ${body.error?.code ?? "error"}:${colors.reset} ${body.error?.message ?? "Unknown error"}`);
1082
+ process.exitCode = 1;
1083
+ }
1084
+ }
1085
+ async function getAuthenticatedClient() {
1086
+ const envToken = process.env.AIDENT_TOKEN;
1087
+ const envBaseUrl = process.env.AIDENT_BASE_URL?.trim();
1088
+ if (envToken) {
1089
+ const baseUrl = envBaseUrl || (await readCredentials())?.base_url || await resolveDefaultBaseUrl();
1090
+ return new CliClient({
1091
+ base_url: normalizeBaseUrl(baseUrl),
1092
+ client_id: "",
1093
+ access_token: envToken
1094
+ });
1095
+ }
1096
+ let creds = await readCredentials();
1097
+ if (!creds)
1098
+ return null;
1099
+ if (envBaseUrl && normalizeBaseUrl(envBaseUrl) !== creds.base_url) {
1100
+ logErr(`${colors.yellow}Warning:${colors.reset} AIDENT_BASE_URL=${envBaseUrl} does not match the host your token was issued for (${creds.base_url}). Ignoring the env override; run \`aident login --base-url ${envBaseUrl}\` to authenticate against the new host.`);
1101
+ }
1102
+ if (isExpired(creds)) {
1103
+ const refreshed = await refreshToken(creds);
1104
+ if (refreshed) {
1105
+ await writeCredentials(refreshed);
1106
+ creds = refreshed;
1107
+ }
1108
+ }
1109
+ return new CliClient(creds);
1110
+ }
1111
+ var catalogCache = null;
1112
+ async function fetchCatalog(client) {
1113
+ if (catalogCache)
1114
+ return catalogCache;
1115
+ const res = await callWithRefresh(client, (c) => c.getCatalog());
1116
+ if (res.status === 401) {
1117
+ logErr(`Not authenticated (HTTP ${res.status}). Run \`aident login\`.`);
1118
+ process.exitCode = 1;
1119
+ return null;
1120
+ }
1121
+ if (res.status !== 200 || !isCommandCatalog(res.body)) {
1122
+ logErr(`Failed to fetch command catalog (HTTP ${res.status}): ${JSON.stringify(res.body)}`);
1123
+ process.exitCode = 1;
1124
+ return null;
1125
+ }
1126
+ catalogCache = res.body;
1127
+ return catalogCache;
1128
+ }
1129
+ function isCommandCatalog(body) {
1130
+ if (!body || typeof body !== "object")
1131
+ return false;
1132
+ const b = body;
1133
+ return Array.isArray(b.packages) && Array.isArray(b.domains) && Array.isArray(b.commands);
1134
+ }
1135
+ main().catch((err) => {
1136
+ logErr(`${colors.red}Error:${colors.reset} ${err instanceof Error ? err.message : String(err)}`);
1137
+ process.exitCode = 1;
1138
+ });
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@aident-ai/cli",
3
+ "version": "0.0.1",
4
+ "description": "Aident CLI — agent-friendly access to 1000+ integrations, playbooks, and the Aident automation platform.",
5
+ "homepage": "https://aident.ai",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Aident-AI/aident.ai.git",
9
+ "directory": "apps/cli"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/Aident-AI/aident.ai/issues"
13
+ },
14
+ "license": "MIT",
15
+ "author": "Aident AI",
16
+ "type": "module",
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "bin": {
21
+ "aident": "./dist/cli.mjs"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "imports": {
29
+ "~cli/*": "./src/*"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22.0.0",
33
+ "typescript": "^5.3.3",
34
+ "@aident/typescript-config": "0.0.0",
35
+ "@aident/eslint-config": "1.0.0",
36
+ "@aident/prettier-config": "1.0.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "keywords": [
42
+ "aident",
43
+ "cli",
44
+ "automation",
45
+ "integrations",
46
+ "playbooks",
47
+ "agent",
48
+ "ai",
49
+ "skill",
50
+ "mcp"
51
+ ],
52
+ "scripts": {
53
+ "build": "bun run scripts/build.ts",
54
+ "dev": "bun run src/cli.ts",
55
+ "test": "bun test",
56
+ "typecheck": "tsc --noEmit",
57
+ "format": "pnpm exec prettier . --write",
58
+ "lint": "pnpm exec eslint ./src --cache --cache-location ../../.eslintcache"
59
+ }
60
+ }