aether-code 0.12.0 → 0.14.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/src/setup.js CHANGED
@@ -1,139 +1,139 @@
1
- // First-run / no-key setup flow.
2
- //
3
- // UX mirrors `gh auth login` / `railway login` / `npm login`:
4
- // 1. Print a friendly explanation
5
- // 2. Open the browser to https://trynoguard.com/account
6
- // 3. Prompt the user to paste the ak_live_ key
7
- // 4. Validate format + verify against /api/v1/me
8
- // 5. Save to ~/.aetherrc with mode 0600
9
- //
10
- // If the user is in a non-TTY environment (CI, piped stdin), we skip the
11
- // auto-open and just print clear instructions for AETHER_API_KEY env var.
12
-
13
- import readline from "node:readline";
14
- import { spawn } from "node:child_process";
15
- import { writeConfigFile, CONFIG_PATH } from "./config.js";
16
- import { fetchBalance, AetherError } from "./api.js";
17
- import { c, errorLine } from "./render.js";
18
-
19
- const ACCOUNT_URL = "https://trynoguard.com/account";
20
- const SIGNUP_URL = "https://trynoguard.com/signup";
21
-
22
- /**
23
- * Cross-platform "open this URL in the user's default browser".
24
- * Best-effort — if it fails (no GUI, blocked, etc.) we just continue.
25
- */
26
- function openInBrowser(url) {
27
- try {
28
- const platform = process.platform;
29
- if (platform === "win32") {
30
- // The empty "" is the window title — required because cmd parses the
31
- // first quoted arg as the title otherwise.
32
- spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
33
- } else if (platform === "darwin") {
34
- spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
35
- } else {
36
- spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
37
- }
38
- return true;
39
- } catch {
40
- return false;
41
- }
42
- }
43
-
44
- function ask(rl, question) {
45
- return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
46
- }
47
-
48
- /**
49
- * Returns true if setup completed successfully (key saved + verified).
50
- * Returns false if the user gave up.
51
- */
52
- export async function runSetup() {
53
- console.log("");
54
- console.log(c.bold(c.magenta("aether")) + c.gray(" — first-time setup"));
55
- console.log(c.gray("─".repeat(60)));
56
- console.log("");
57
- console.log("To use Aether, you need an API key tied to your account.");
58
- console.log("Keys start with " + c.cyan("ak_live_") + ".");
59
- console.log("");
60
-
61
- // Non-TTY — bail with instructions instead of prompting
62
- if (!process.stdin.isTTY) {
63
- console.log(errorLine("Can't run interactive setup (stdin isn't a TTY)."));
64
- console.log("");
65
- console.log("Options:");
66
- console.log(` · Set ${c.cyan("AETHER_API_KEY")} env var to your ak_live_ key.`);
67
- console.log(` · Or run ${c.cyan("aether config set <key>")} from a real terminal.`);
68
- console.log(` · Get a key at ${c.blue(ACCOUNT_URL)}.`);
69
- console.log("");
70
- return false;
71
- }
72
-
73
- console.log(c.bold("Step 1: ") + "Open " + c.blue(ACCOUNT_URL) + " in your browser.");
74
- console.log(c.gray(" (no account yet? sign up free at " + SIGNUP_URL + ")"));
75
-
76
- const opened = openInBrowser(ACCOUNT_URL);
77
- if (opened) {
78
- console.log(c.gray(" ↪ opened it for you."));
79
- }
80
- console.log("");
81
- console.log(c.bold("Step 2: ") + "Click " + c.cyan("Generate API key") + " and copy the key shown.");
82
- console.log(c.gray(" (the key is shown ONCE — copy it before navigating away)"));
83
- console.log("");
84
- console.log(c.bold("Step 3: ") + "Paste the key below.");
85
- console.log("");
86
-
87
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
88
-
89
- // Up to 3 attempts at a valid key
90
- let saved = false;
91
- for (let attempt = 0; attempt < 3; attempt++) {
92
- const key = await ask(rl, c.magenta("API key: "));
93
- if (!key) {
94
- console.log(c.gray("(empty — skipping)"));
95
- break;
96
- }
97
- if (key.toLowerCase() === "q" || key.toLowerCase() === "quit") {
98
- console.log(c.gray("Cancelled."));
99
- break;
100
- }
101
- if (!key.startsWith("ak_live_")) {
102
- console.log(errorLine(`Keys start with ${c.cyan("ak_live_")} — that doesn't look right. Try again or type 'q' to cancel.`));
103
- continue;
104
- }
105
- if (key.length < 30) {
106
- console.log(errorLine("That key looks too short. Try copying again."));
107
- continue;
108
- }
109
-
110
- // Tentative save so the API client picks it up
111
- writeConfigFile({ apiKey: key });
112
- process.stdout.write(c.gray("Verifying..."));
113
- try {
114
- const me = await fetchBalance();
115
- console.log(c.green(" ✓"));
116
- console.log("");
117
- console.log(c.green(c.bold("Setup complete.")));
118
- console.log(
119
- c.gray(`Saved to ${CONFIG_PATH} (mode 0600).`) +
120
- c.gray(`\nPlan: ${me.plan} · Balance: ${me.balance.toLocaleString()} credits`),
121
- );
122
- console.log("");
123
- saved = true;
124
- break;
125
- } catch (err) {
126
- console.log(c.red(" ✗"));
127
- if (err instanceof AetherError && err.status === 401) {
128
- console.log(errorLine("Server rejected that key (401). Double-check you copied it correctly."));
129
- } else {
130
- console.log(errorLine(err.message || String(err)));
131
- }
132
- // Roll back the bad save so we don't leave a broken key on disk
133
- writeConfigFile({ apiKey: "" });
134
- }
135
- }
136
-
137
- rl.close();
138
- return saved;
139
- }
1
+ // First-run / no-key setup flow.
2
+ //
3
+ // UX mirrors `gh auth login` / `railway login` / `npm login`:
4
+ // 1. Print a friendly explanation
5
+ // 2. Open the browser to https://trynoguard.com/account
6
+ // 3. Prompt the user to paste the ak_live_ key
7
+ // 4. Validate format + verify against /api/v1/me
8
+ // 5. Save to ~/.aetherrc with mode 0600
9
+ //
10
+ // If the user is in a non-TTY environment (CI, piped stdin), we skip the
11
+ // auto-open and just print clear instructions for AETHER_API_KEY env var.
12
+
13
+ import readline from "node:readline";
14
+ import { spawn } from "node:child_process";
15
+ import { writeConfigFile, CONFIG_PATH } from "./config.js";
16
+ import { fetchBalance, AetherError } from "./api.js";
17
+ import { c, errorLine } from "./render.js";
18
+
19
+ const ACCOUNT_URL = "https://trynoguard.com/account";
20
+ const SIGNUP_URL = "https://trynoguard.com/signup";
21
+
22
+ /**
23
+ * Cross-platform "open this URL in the user's default browser".
24
+ * Best-effort — if it fails (no GUI, blocked, etc.) we just continue.
25
+ */
26
+ function openInBrowser(url) {
27
+ try {
28
+ const platform = process.platform;
29
+ if (platform === "win32") {
30
+ // The empty "" is the window title — required because cmd parses the
31
+ // first quoted arg as the title otherwise.
32
+ spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
33
+ } else if (platform === "darwin") {
34
+ spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
35
+ } else {
36
+ spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
37
+ }
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ function ask(rl, question) {
45
+ return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
46
+ }
47
+
48
+ /**
49
+ * Returns true if setup completed successfully (key saved + verified).
50
+ * Returns false if the user gave up.
51
+ */
52
+ export async function runSetup() {
53
+ console.log("");
54
+ console.log(c.bold(c.magenta("aether")) + c.gray(" — first-time setup"));
55
+ console.log(c.gray("─".repeat(60)));
56
+ console.log("");
57
+ console.log("To use Aether, you need an API key tied to your account.");
58
+ console.log("Keys start with " + c.cyan("ak_live_") + ".");
59
+ console.log("");
60
+
61
+ // Non-TTY — bail with instructions instead of prompting
62
+ if (!process.stdin.isTTY) {
63
+ console.log(errorLine("Can't run interactive setup (stdin isn't a TTY)."));
64
+ console.log("");
65
+ console.log("Options:");
66
+ console.log(` · Set ${c.cyan("AETHER_API_KEY")} env var to your ak_live_ key.`);
67
+ console.log(` · Or run ${c.cyan("aether config set <key>")} from a real terminal.`);
68
+ console.log(` · Get a key at ${c.blue(ACCOUNT_URL)}.`);
69
+ console.log("");
70
+ return false;
71
+ }
72
+
73
+ console.log(c.bold("Step 1: ") + "Open " + c.blue(ACCOUNT_URL) + " in your browser.");
74
+ console.log(c.gray(" (no account yet? sign up free at " + SIGNUP_URL + ")"));
75
+
76
+ const opened = openInBrowser(ACCOUNT_URL);
77
+ if (opened) {
78
+ console.log(c.gray(" ↪ opened it for you."));
79
+ }
80
+ console.log("");
81
+ console.log(c.bold("Step 2: ") + "Click " + c.cyan("Generate API key") + " and copy the key shown.");
82
+ console.log(c.gray(" (the key is shown ONCE — copy it before navigating away)"));
83
+ console.log("");
84
+ console.log(c.bold("Step 3: ") + "Paste the key below.");
85
+ console.log("");
86
+
87
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
88
+
89
+ // Up to 3 attempts at a valid key
90
+ let saved = false;
91
+ for (let attempt = 0; attempt < 3; attempt++) {
92
+ const key = await ask(rl, c.magenta("API key: "));
93
+ if (!key) {
94
+ console.log(c.gray("(empty — skipping)"));
95
+ break;
96
+ }
97
+ if (key.toLowerCase() === "q" || key.toLowerCase() === "quit") {
98
+ console.log(c.gray("Cancelled."));
99
+ break;
100
+ }
101
+ if (!key.startsWith("ak_live_")) {
102
+ console.log(errorLine(`Keys start with ${c.cyan("ak_live_")} — that doesn't look right. Try again or type 'q' to cancel.`));
103
+ continue;
104
+ }
105
+ if (key.length < 30) {
106
+ console.log(errorLine("That key looks too short. Try copying again."));
107
+ continue;
108
+ }
109
+
110
+ // Tentative save so the API client picks it up
111
+ writeConfigFile({ apiKey: key });
112
+ process.stdout.write(c.gray("Verifying..."));
113
+ try {
114
+ const me = await fetchBalance();
115
+ console.log(c.green(" ✓"));
116
+ console.log("");
117
+ console.log(c.green(c.bold("Setup complete.")));
118
+ console.log(
119
+ c.gray(`Saved to ${CONFIG_PATH} (mode 0600).`) +
120
+ c.gray(`\nPlan: ${me.plan} · Balance: ${me.balance.toLocaleString()} credits`),
121
+ );
122
+ console.log("");
123
+ saved = true;
124
+ break;
125
+ } catch (err) {
126
+ console.log(c.red(" ✗"));
127
+ if (err instanceof AetherError && err.status === 401) {
128
+ console.log(errorLine("Server rejected that key (401). Double-check you copied it correctly."));
129
+ } else {
130
+ console.log(errorLine(err.message || String(err)));
131
+ }
132
+ // Roll back the bad save so we don't leave a broken key on disk
133
+ writeConfigFile({ apiKey: "" });
134
+ }
135
+ }
136
+
137
+ rl.close();
138
+ return saved;
139
+ }
package/src/skills.js CHANGED
@@ -41,6 +41,9 @@ export function parseSkill(raw, sourcePath = "<inline>") {
41
41
  if (typeof raw !== "string" || raw.length === 0) {
42
42
  throw new Error(`Skill ${sourcePath}: empty content`);
43
43
  }
44
+ // Tolerate CRLF (Windows-authored skills / editors) — normalize to LF so the
45
+ // frontmatter delimiter and downstream line parsing don't choke on '\r'.
46
+ raw = raw.replace(/\r\n/g, "\n");
44
47
  if (!raw.startsWith("---")) {
45
48
  throw new Error(`Skill ${sourcePath}: missing YAML frontmatter (must start with '---')`);
46
49
  }