@amdahl/cli 0.3.0 → 0.3.2

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 (3) hide show
  1. package/README.md +35 -16
  2. package/dist/amdahl.js +386 -153
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -60,36 +60,48 @@ The quickest way to start is `npx amdahl` with nothing after it, in a
60
60
  terminal. It walks you through setup:
61
61
 
62
62
  ```
63
- Amdahl, predictive ML for GTM
64
- Found on this machine: Claude Code ✓ Cursor ✓ Codex –
65
- Set up Amdahl in:
63
+ $ npx amdahl
64
+ Amdahl · predictive ML for GTM
65
+ Set up in:
66
66
  > [x] Claude Code
67
- [x] Cursor
67
+ [ ] Cursor
68
68
  [ ] Codex
69
69
  (Use the arrow keys, Space to tick or untick, then Enter)
70
+ Opening your browser…
71
+ ✓ Signed in · Acme
72
+ ✓ Added to Claude Code
73
+ Try it: in Claude Code, run /mcp once to connect, then say "make this email sound less like AI"
70
74
  ```
71
75
 
72
76
  It finds Claude Code, Cursor and Codex by their command on your PATH
73
77
  (`claude`, `cursor`, `codex`) or their folder in your home folder (`.claude`,
74
78
  `.cursor`, `.codex`), and ticks the ones it finds. Enter accepts. If you are
75
- not signed in, it signs you in through the browser first; otherwise it prints
76
- `Signed in as <email> (workspace: <name>)`. If the server rejects a key set in
79
+ not signed in, it signs you in through the browser first. It prints the
80
+ sign-in link only if the browser could not be opened, or nothing came back
81
+ within a minute, and it waits up to 15 minutes, so someone new can create
82
+ their workspace on the way and land back in the same setup. If the server
83
+ rejects a key set in
77
84
  `AMDAHL_KEY` (or `AMDAHL_API_KEY`, `AMDAHL_ACCESS_TOKEN`, `--api-key`), it says
78
85
  so and skips the browser sign-in, since that key would still win. Then it
79
- runs `amdahl install` for each client you ticked. A client that fails prints
86
+ sets up each client you ticked the way `amdahl install` does, one line each.
87
+ A client that fails prints
80
88
  its error and the rest still run; the exit code is the first failure's. It
81
- ends with something to try, such as
82
- `Try it: open Claude Code and say "make this email sound less like AI"`
83
- (for Codex, it first says to run `codex mcp login amdahl`).
89
+ ends with something to try in the first client it set up. Each client then
90
+ signs in to Amdahl itself, once, the first time it connects: `/mcp` in
91
+ Claude Code, `codex mcp login amdahl` for Codex (always named, since the CLI
92
+ cannot tell whether it ran), and Cursor asks on first use.
93
+ `npx amdahl --verbose` also prints the files it wrote, the profile and where
94
+ the sign-in is stored.
84
95
  Run it again once you are set up and it says `You're set up in Claude Code
85
- and Cursor.` with the same line to try, and asks nothing; if it cannot reach
96
+ and Cursor.` with a line to try, and asks nothing; if it cannot reach
86
97
  Amdahl to check your sign-in, it still says so. Ctrl+C at the question stops
87
98
  it with nothing written (exit 130).
88
99
 
89
100
  It only does this for a person at a terminal. When stdin, stdout or stderr
90
101
  (where the menu draws) is not a terminal, `CI` is set, an agent runs it (`CLAUDECODE` or `CURSOR_AGENT` is
91
- set), `AMDAHL_NO_PROMPT=1` is set, or any flag is given (such as `--json`),
92
- bare `amdahl` prints the usage to stderr and exits 2, as before.
102
+ set), `AMDAHL_NO_PROMPT=1` is set, or any flag other than `--verbose` is given
103
+ (such as `--json`), bare `amdahl` prints the usage to stderr and exits 2, as
104
+ before.
93
105
 
94
106
  To connect one AI client yourself, run `amdahl install claude-code` (or `codex`,
95
107
  `cursor`). It adds the Amdahl MCP server and also installs the Amdahl skill,
@@ -124,8 +136,14 @@ amdahl install claude-code # connect an AI client and add the Amdahl skill (al
124
136
  amdahl skills add <publisher>/<skill> # add a skill from amdahl.ai/skills to this project
125
137
  ```
126
138
 
127
- No workspace yet? `amdahl login` prints the console link where you can
128
- create one or join the beta. The CLI never creates or joins a workspace.
139
+ No workspace yet? With a work email, the sign-in page offers to create your
140
+ company's workspace and then finishes signing you in. If you signed in with
141
+ an address Amdahl can't use, the browser tab and the terminal say what to do
142
+ and link a new sign-in ("Sign in with your work email", "Sign in again" for
143
+ a subdomain address, "Try again" once you've confirmed your email), and the
144
+ terminal keeps waiting for it. Anything
145
+ else exits 5 with the one next step (ask your company's admins, try again
146
+ tomorrow, and so on). The CLI itself never creates or joins a workspace.
129
147
 
130
148
  On a server or in CI, skip `login` and set an API key in `AMDAHL_KEY`.
131
149
 
@@ -325,7 +343,8 @@ the default profile). `whoami` names the source that won. `keys create`,
325
343
  tokens.
326
344
  - Tokens: the OS keychain (service `amdahl-cli`; macOS `security`, Linux
327
345
  `secret-tool`), passed on stdin, never on argv. Without a keychain:
328
- `credentials.json` (mode 0600) with a one-time warning.
346
+ `credentials.json` (mode 0600), named under `--verbose`. If that file is
347
+ ever readable by other users, the CLI makes it owner-only and warns.
329
348
  `AMDAHL_CREDENTIAL_STORE=keychain|file` forces either.
330
349
 
331
350
  ## Keys
package/dist/amdahl.js CHANGED
@@ -1000,14 +1000,14 @@ var require_foldFlowLines = __commonJS({
1000
1000
  var FOLD_FLOW = "flow";
1001
1001
  var FOLD_BLOCK = "block";
1002
1002
  var FOLD_QUOTED = "quoted";
1003
- function foldFlowLines(text, indent, mode = "flow", { indentAtStart, lineWidth = 80, minContentWidth = 20, onFold, onOverflow } = {}) {
1003
+ function foldFlowLines(text2, indent, mode = "flow", { indentAtStart, lineWidth = 80, minContentWidth = 20, onFold, onOverflow } = {}) {
1004
1004
  if (!lineWidth || lineWidth < 0)
1005
- return text;
1005
+ return text2;
1006
1006
  if (lineWidth < minContentWidth)
1007
1007
  minContentWidth = 0;
1008
1008
  const endStep = Math.max(1 + minContentWidth, 1 + lineWidth - indent.length);
1009
- if (text.length <= endStep)
1010
- return text;
1009
+ if (text2.length <= endStep)
1010
+ return text2;
1011
1011
  const folds = [];
1012
1012
  const escapedFolds = {};
1013
1013
  let end = lineWidth - indent.length;
@@ -1024,14 +1024,14 @@ var require_foldFlowLines = __commonJS({
1024
1024
  let escStart = -1;
1025
1025
  let escEnd = -1;
1026
1026
  if (mode === FOLD_BLOCK) {
1027
- i = consumeMoreIndentedLines(text, i, indent.length);
1027
+ i = consumeMoreIndentedLines(text2, i, indent.length);
1028
1028
  if (i !== -1)
1029
1029
  end = i + endStep;
1030
1030
  }
1031
- for (let ch; ch = text[i += 1]; ) {
1031
+ for (let ch; ch = text2[i += 1]; ) {
1032
1032
  if (mode === FOLD_QUOTED && ch === "\\") {
1033
1033
  escStart = i;
1034
- switch (text[i + 1]) {
1034
+ switch (text2[i + 1]) {
1035
1035
  case "x":
1036
1036
  i += 3;
1037
1037
  break;
@@ -1048,12 +1048,12 @@ var require_foldFlowLines = __commonJS({
1048
1048
  }
1049
1049
  if (ch === "\n") {
1050
1050
  if (mode === FOLD_BLOCK)
1051
- i = consumeMoreIndentedLines(text, i, indent.length);
1051
+ i = consumeMoreIndentedLines(text2, i, indent.length);
1052
1052
  end = i + indent.length + endStep;
1053
1053
  split = void 0;
1054
1054
  } else {
1055
1055
  if (ch === " " && prev && prev !== " " && prev !== "\n" && prev !== " ") {
1056
- const next = text[i + 1];
1056
+ const next = text2[i + 1];
1057
1057
  if (next && next !== " " && next !== "\n" && next !== " ")
1058
1058
  split = i;
1059
1059
  }
@@ -1065,12 +1065,12 @@ var require_foldFlowLines = __commonJS({
1065
1065
  } else if (mode === FOLD_QUOTED) {
1066
1066
  while (prev === " " || prev === " ") {
1067
1067
  prev = ch;
1068
- ch = text[i += 1];
1068
+ ch = text2[i += 1];
1069
1069
  overflow = true;
1070
1070
  }
1071
1071
  const j = i > escEnd + 1 ? i - 2 : escStart - 1;
1072
1072
  if (escapedFolds[j])
1073
- return text;
1073
+ return text2;
1074
1074
  folds.push(j);
1075
1075
  escapedFolds[j] = true;
1076
1076
  end = j + endStep;
@@ -1085,39 +1085,39 @@ var require_foldFlowLines = __commonJS({
1085
1085
  if (overflow && onOverflow)
1086
1086
  onOverflow();
1087
1087
  if (folds.length === 0)
1088
- return text;
1088
+ return text2;
1089
1089
  if (onFold)
1090
1090
  onFold();
1091
- let res = text.slice(0, folds[0]);
1091
+ let res = text2.slice(0, folds[0]);
1092
1092
  for (let i2 = 0; i2 < folds.length; ++i2) {
1093
1093
  const fold = folds[i2];
1094
- const end2 = folds[i2 + 1] || text.length;
1094
+ const end2 = folds[i2 + 1] || text2.length;
1095
1095
  if (fold === 0)
1096
1096
  res = `
1097
- ${indent}${text.slice(0, end2)}`;
1097
+ ${indent}${text2.slice(0, end2)}`;
1098
1098
  else {
1099
1099
  if (mode === FOLD_QUOTED && escapedFolds[fold])
1100
- res += `${text[fold]}\\`;
1100
+ res += `${text2[fold]}\\`;
1101
1101
  res += `
1102
- ${indent}${text.slice(fold + 1, end2)}`;
1102
+ ${indent}${text2.slice(fold + 1, end2)}`;
1103
1103
  }
1104
1104
  }
1105
1105
  return res;
1106
1106
  }
1107
- function consumeMoreIndentedLines(text, i, indent) {
1107
+ function consumeMoreIndentedLines(text2, i, indent) {
1108
1108
  let end = i;
1109
1109
  let start = i + 1;
1110
- let ch = text[start];
1110
+ let ch = text2[start];
1111
1111
  while (ch === " " || ch === " ") {
1112
1112
  if (i < start + indent) {
1113
- ch = text[++i];
1113
+ ch = text2[++i];
1114
1114
  } else {
1115
1115
  do {
1116
- ch = text[++i];
1116
+ ch = text2[++i];
1117
1117
  } while (ch && ch !== "\n");
1118
1118
  end = i;
1119
1119
  start = i + 1;
1120
- ch = text[start];
1120
+ ch = text2[start];
1121
1121
  }
1122
1122
  }
1123
1123
  return end;
@@ -6347,15 +6347,15 @@ var require_parser = __commonJS({
6347
6347
  var node_process = __require("process");
6348
6348
  var cst = require_cst();
6349
6349
  var lexer = require_lexer();
6350
- function includesToken(list, type) {
6351
- for (let i = 0; i < list.length; ++i)
6352
- if (list[i].type === type)
6350
+ function includesToken(list2, type) {
6351
+ for (let i = 0; i < list2.length; ++i)
6352
+ if (list2[i].type === type)
6353
6353
  return true;
6354
6354
  return false;
6355
6355
  }
6356
- function findNonEmptyIndex(list) {
6357
- for (let i = 0; i < list.length; ++i) {
6358
- switch (list[i].type) {
6356
+ function findNonEmptyIndex(list2) {
6357
+ for (let i = 0; i < list2.length; ++i) {
6358
+ switch (list2[i].type) {
6359
6359
  case "space":
6360
6360
  case "comment":
6361
6361
  case "newline":
@@ -7406,10 +7406,10 @@ ${SHOW_CURSOR}`);
7406
7406
  reject(outcome.error);
7407
7407
  }
7408
7408
  };
7409
- const onKeypress = (text, key) => {
7410
- const name = key?.name ?? text;
7409
+ const onKeypress = (text2, key) => {
7410
+ const name = key?.name ?? text2;
7411
7411
  if (key?.ctrl && (name === "c" || name === "d")) return finish({ error: new PromptCancelled() });
7412
- const result = onKey(text, name);
7412
+ const result = onKey(text2, name);
7413
7413
  if (result === "redraw") output.write(`${up(height + 1)}${draw()}`);
7414
7414
  else if (result) finish(result);
7415
7415
  };
@@ -7432,19 +7432,19 @@ ${draw()}`);
7432
7432
  function marker(options) {
7433
7433
  return options.color ? options.trueColor ? "\x1B[1;38;2;253;75;3m" : "\x1B[1;38;5;202m" : "\x1B[1m";
7434
7434
  }
7435
- function move(current, count, text, name) {
7435
+ function move(current, count, text2, name) {
7436
7436
  if (name === "up" || name === "k") return (current - 1 + count) % count;
7437
7437
  if (name === "down" || name === "j") return (current + 1) % count;
7438
- if (text && /^[1-9]$/.test(text) && Number(text) <= count) return Number(text) - 1;
7438
+ if (text2 && /^[1-9]$/.test(text2) && Number(text2) <= count) return Number(text2) - 1;
7439
7439
  return null;
7440
7440
  }
7441
7441
  function selectMenu(input, output, question, choices, options = {}) {
7442
7442
  let current = Math.min(Math.max(options.initial ?? 0, 0), choices.length - 1);
7443
7443
  const on = marker(options);
7444
7444
  const rows = () => choices.map((c, i) => i === current ? `${on}> ${c}${OFF}` : ` ${c}`);
7445
- return runMenu(input, output, question, HINT, rows, (text, name) => {
7445
+ return runMenu(input, output, question, HINT, rows, (text2, name) => {
7446
7446
  if (name === "return" || name === "enter") return { pick: current, summary: choices[current] ?? "" };
7447
- const next = move(current, choices.length, text, name);
7447
+ const next = move(current, choices.length, text2, name);
7448
7448
  if (next === null || next === current) return null;
7449
7449
  current = next;
7450
7450
  return "redraw";
@@ -7458,7 +7458,7 @@ function multiSelectMenu(input, output, question, choices, options = {}) {
7458
7458
  const row = `[${ticked[i] ? "x" : " "}] ${c}`;
7459
7459
  return i === current ? `${on}> ${row}${OFF}` : ` ${row}`;
7460
7460
  });
7461
- return runMenu(input, output, question, MULTI_HINT, rows, (text, name) => {
7461
+ return runMenu(input, output, question, MULTI_HINT, rows, (text2, name) => {
7462
7462
  if (name === "return" || name === "enter") {
7463
7463
  const picks = choices.flatMap((_, i) => ticked[i] ? [i] : []);
7464
7464
  return { pick: picks, summary: picks.map((i) => choices[i]).join(", ") || "none" };
@@ -7467,12 +7467,12 @@ function multiSelectMenu(input, output, question, choices, options = {}) {
7467
7467
  ticked[current] = !ticked[current];
7468
7468
  return "redraw";
7469
7469
  }
7470
- if (text && /^[1-9]$/.test(text) && Number(text) <= choices.length) {
7471
- current = Number(text) - 1;
7470
+ if (text2 && /^[1-9]$/.test(text2) && Number(text2) <= choices.length) {
7471
+ current = Number(text2) - 1;
7472
7472
  ticked[current] = !ticked[current];
7473
7473
  return "redraw";
7474
7474
  }
7475
- const next = move(current, choices.length, text, name);
7475
+ const next = move(current, choices.length, text2, name);
7476
7476
  if (next === null || next === current) return null;
7477
7477
  current = next;
7478
7478
  return "redraw";
@@ -7548,8 +7548,8 @@ function openerFor(platform, url) {
7548
7548
  function realIo() {
7549
7549
  return {
7550
7550
  env: process.env,
7551
- stdout: (text) => process.stdout.write(text),
7552
- stderr: (text) => process.stderr.write(text),
7551
+ stdout: (text2) => process.stdout.write(text2),
7552
+ stderr: (text2) => process.stderr.write(text2),
7553
7553
  stdinIsTTY: Boolean(process.stdin.isTTY),
7554
7554
  stdoutIsTTY: Boolean(process.stdout.isTTY),
7555
7555
  stderrIsTTY: Boolean(process.stderr.isTTY),
@@ -7567,7 +7567,7 @@ function realIo() {
7567
7567
  run: runProcess,
7568
7568
  openUrl: async (url) => {
7569
7569
  const [cmd, args] = openerFor(process.platform, url);
7570
- await runProcess(cmd, args);
7570
+ return (await runProcess(cmd, args)).code === 0;
7571
7571
  },
7572
7572
  homedir: homedir(),
7573
7573
  cwd: process.cwd(),
@@ -7575,7 +7575,8 @@ function realIo() {
7575
7575
  hostname: hostname(),
7576
7576
  now: () => Date.now(),
7577
7577
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
7578
- loginTimeoutMs: 3e5,
7578
+ loginTimeoutMs: 9e5,
7579
+ browserGraceMs: 6e4,
7579
7580
  fetchTimeoutMs: 15e3
7580
7581
  };
7581
7582
  }
@@ -7654,7 +7655,8 @@ var GLOBAL_OPTIONS = {
7654
7655
  json: { type: "boolean" },
7655
7656
  "no-color": { type: "boolean" },
7656
7657
  help: { type: "boolean", short: "h" },
7657
- version: { type: "boolean", short: "v" }
7658
+ version: { type: "boolean", short: "v" },
7659
+ verbose: { type: "boolean" }
7658
7660
  };
7659
7661
  var COMMAND_OPTIONS = {
7660
7662
  login: {
@@ -7753,6 +7755,7 @@ var USAGE = `Usage: amdahl <command> [options]
7753
7755
  What you can do:
7754
7756
  amdahl Set up Claude Code, Cursor or Codex, in a terminal: it asks
7755
7757
  which, signs you in, and runs install for each one
7758
+ (--verbose shows the files and profile it used)
7756
7759
  amdahl optimize draft.md Rewrite an outbound email or LinkedIn message
7757
7760
  amdahl status Check that this workspace is set up to optimize
7758
7761
  amdahl keys create --name ci Make an API key for a server or CI
@@ -7793,6 +7796,7 @@ Global options:
7793
7796
  --api-key <key> Use an API key for this command (prefer AMDAHL_KEY: argv is visible to other users)
7794
7797
  --json Data on stdout as JSON, messages on stderr
7795
7798
  --no-color No colour (NO_COLOR is honoured too)
7799
+ --verbose Say more: files written, the profile, where the sign-in is stored
7796
7800
  -h, --help Show this help
7797
7801
  -v, --version Show the version
7798
7802
  `;
@@ -8087,13 +8091,55 @@ function authorizeUrl(endpoints, input) {
8087
8091
  const query = params.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
8088
8092
  return `${endpoints.authorization_endpoint}?${query}`;
8089
8093
  }
8090
- function html(text) {
8091
- return text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
8094
+ var SIGN_IN_AGAIN = {
8095
+ not_work_email: { why: "Amdahl needs your work email.", label: "Sign in with your work email" },
8096
+ subdomain_email: { why: "Use your main company email.", label: "Sign in again" },
8097
+ email_unconfirmed: { why: "Check your inbox to confirm your email, then try again.", label: "Try again" }
8098
+ };
8099
+ function signInAgainLine(reason, url) {
8100
+ const again = SIGN_IN_AGAIN[reason];
8101
+ return again ? `${again.why} ${again.label}:
8102
+
8103
+ ${url}
8104
+ ` : "";
8105
+ }
8106
+ function noBusinessMessage(consoleUrl, mayCreate, reason) {
8107
+ switch (reason) {
8108
+ case void 0:
8109
+ return mayCreate ? `You're signed in, but you don't have a workspace yet. Create one at ${consoleUrl}/new, then run \`amdahl login\` again.` : `You're signed in, but you don't have access yet. Amdahl is in beta: join the waitlist at ${consoleUrl}/new, then run \`amdahl login\` again once you're in.`;
8110
+ case "domain_has_workspace":
8111
+ return `Your company already has an Amdahl workspace. Ask its admins to let you in at ${consoleUrl}, then try again.`;
8112
+ case "invite_only":
8113
+ return "Your company's Amdahl workspace is invite-only. Ask a colleague in it to invite you, then try again.";
8114
+ case "not_work_email":
8115
+ case "subdomain_email":
8116
+ case "email_unconfirmed": {
8117
+ const again = SIGN_IN_AGAIN[reason];
8118
+ return `${again.why} Run this again, then choose "${again.label}" in the browser.`;
8119
+ }
8120
+ case "daily_cap":
8121
+ return "Amdahl isn't taking more new workspaces today. Try again tomorrow.";
8122
+ case "paused":
8123
+ return "New workspaces are paused for now. Try again later.";
8124
+ case "domain_used":
8125
+ return "Your company already used its free workspace. Email hello@amdahl.ai to get access.";
8126
+ default:
8127
+ return "Amdahl couldn't create your workspace just now. Try again in a few minutes.";
8128
+ }
8129
+ }
8130
+ function signInAgainUrl(authorize) {
8131
+ const url = new URL(authorize);
8132
+ return `${url.origin}/login?redirect=${encodeURIComponent(`${url.pathname}${url.search}`)}`;
8133
+ }
8134
+ function html(text2) {
8135
+ return text2.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
8092
8136
  }
8093
8137
  function page(body) {
8094
8138
  return `<!doctype html><html><head><meta charset="utf-8"><title>Amdahl CLI</title></head><body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;line-height:1.5">${body}</body></html>`;
8095
8139
  }
8096
8140
  async function startLoopback(io, input) {
8141
+ let authorize = null;
8142
+ let refused = null;
8097
8143
  let settle;
8098
8144
  let fail;
8099
8145
  const result = new Promise((resolve, reject) => {
@@ -8120,9 +8166,18 @@ async function startLoopback(io, input) {
8120
8166
  const error = url.searchParams.get("error");
8121
8167
  const code = url.searchParams.get("code");
8122
8168
  if (error === "no_business") {
8123
- const newUrl = `${input.consoleUrl}/new`;
8124
- send2(200, `<p>You're signed in, but you don't have a workspace yet.</p><p><a href="${html(newUrl)}">Go to ${html(newUrl)}</a>, then run <code>amdahl login</code> again.</p>`);
8125
- settle({ kind: "no_business", mayCreate: url.searchParams.get("may_create") === "true" });
8169
+ const mayCreate = url.searchParams.get("may_create") === "true";
8170
+ const reason = url.searchParams.get("reason") ?? void 0;
8171
+ const again = reason ? SIGN_IN_AGAIN[reason] : void 0;
8172
+ if (again && authorize) {
8173
+ refused = reason;
8174
+ const retry = signInAgainUrl(authorize);
8175
+ send2(200, `<p>${html(again.why)}</p><p><a href="${html(retry)}">${html(again.label)} \u2192</a></p>`);
8176
+ input.onSignInAgain?.(reason, retry);
8177
+ return;
8178
+ }
8179
+ send2(200, `<p>${html(noBusinessMessage(input.consoleUrl, mayCreate, reason))}</p>`);
8180
+ settle({ kind: "no_business", mayCreate, reason });
8126
8181
  } else if (error) {
8127
8182
  send2(400, `<p>Sign-in failed: ${html(url.searchParams.get("error_description") ?? error)}</p>`);
8128
8183
  settle({ kind: "error", error, description: url.searchParams.get("error_description") ?? void 0 });
@@ -8150,12 +8205,17 @@ async function startLoopback(io, input) {
8150
8205
  });
8151
8206
  const port = server.address().port;
8152
8207
  const timer = setTimeout(() => {
8153
- fail(new CliError("login_timeout", "Timed out waiting for the browser sign-in.", EXIT.general, "Run `amdahl login` again."));
8208
+ fail(
8209
+ refused ? new CliError("no_business", noBusinessMessage(input.consoleUrl, false, refused), EXIT.noWorkspace) : new CliError("login_timeout", "Timed out waiting for the browser sign-in.", EXIT.general, "Run `amdahl login` again.")
8210
+ );
8154
8211
  }, input.timeoutMs);
8155
8212
  timer.unref?.();
8156
8213
  return {
8157
8214
  redirectUri: `http://127.0.0.1:${port}/callback`,
8158
8215
  result,
8216
+ setAuthorizeUrl: (url) => {
8217
+ authorize = url;
8218
+ },
8159
8219
  close: () => {
8160
8220
  clearTimeout(timer);
8161
8221
  server.closeAllConnections?.();
@@ -8173,7 +8233,7 @@ function consoleUrlFor(io, apiUrl) {
8173
8233
  }
8174
8234
 
8175
8235
  // src/secrets.ts
8176
- import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync } from "fs";
8236
+ import { chmodSync as chmodSync2, existsSync as existsSync2, readFileSync as readFileSync2, rmSync, statSync } from "fs";
8177
8237
  import { join as join2 } from "path";
8178
8238
  var KEYCHAIN_SERVICE = "amdahl-cli";
8179
8239
  function keychainAccount(apiUrl, profile) {
@@ -8260,10 +8320,28 @@ function readCredentials(path) {
8260
8320
  return { version: 1, accounts: {} };
8261
8321
  }
8262
8322
  }
8263
- function fileStore(io, warn) {
8323
+ function fileStore(io, note) {
8264
8324
  const path = credentialsPath(io);
8325
+ let checked = false;
8326
+ const checkMode = () => {
8327
+ if (checked || io.platform === "win32" || !existsSync2(path)) return;
8328
+ checked = true;
8329
+ if ((statSync(path).mode & 63) === 0) return;
8330
+ try {
8331
+ chmodSync2(path, 384);
8332
+ } catch {
8333
+ io.stderr(`Warning: ${path} can be read by other users on this machine, and this CLI cannot change that. Run \`chmod 600 ${path}\` as its owner.
8334
+ `);
8335
+ return;
8336
+ }
8337
+ io.stderr(
8338
+ `Warning: ${path} was readable by other users on this machine. It is private now; to be safe, run \`amdahl logout\` and sign in again.
8339
+ `
8340
+ );
8341
+ };
8265
8342
  return {
8266
8343
  async get(account) {
8344
+ checkMode();
8267
8345
  return asSecrets(readCredentials(path).accounts[account]);
8268
8346
  },
8269
8347
  async set(account, secrets) {
@@ -8271,11 +8349,9 @@ function fileStore(io, warn) {
8271
8349
  const file = readCredentials(path);
8272
8350
  file.accounts[account] = secrets;
8273
8351
  writePrivateJson(path, file);
8274
- if (!existed && warn) {
8275
- io.stderr(
8276
- `Warning: no OS keychain is available, so your sign-in is stored in ${path} (readable only by you).
8277
- `
8278
- );
8352
+ if (!existed && note) {
8353
+ io.stderr(`No OS keychain here, so your sign-in is stored in ${path} (readable only by you).
8354
+ `);
8279
8355
  }
8280
8356
  },
8281
8357
  async delete(account) {
@@ -8287,7 +8363,7 @@ function fileStore(io, warn) {
8287
8363
  }
8288
8364
  };
8289
8365
  }
8290
- function createSecretStore(io) {
8366
+ function createSecretStore(io, options = {}) {
8291
8367
  const forced = io.env.AMDAHL_CREDENTIAL_STORE;
8292
8368
  if (forced && forced !== "keychain" && forced !== "file") {
8293
8369
  throw new CliError(
@@ -8314,7 +8390,7 @@ function createSecretStore(io) {
8314
8390
  }
8315
8391
  };
8316
8392
  }
8317
- const file = fileStore(io, true);
8393
+ const file = fileStore(io, options.verbose === true);
8318
8394
  if (!keychain) return file;
8319
8395
  return {
8320
8396
  async get(account) {
@@ -8366,7 +8442,7 @@ function createCtx(io, out, flags) {
8366
8442
  profile,
8367
8443
  apiUrl: normaliseApiUrl(rawApi),
8368
8444
  nonInteractive: !io.stdinIsTTY || io.env.AMDAHL_NO_PROMPT === "1",
8369
- secrets: () => store ??= createSecretStore(io),
8445
+ secrets: () => store ??= createSecretStore(io, { verbose: flags.verbose === true }),
8370
8446
  saveConfig: () => saveConfig(io, config)
8371
8447
  };
8372
8448
  }
@@ -8464,15 +8540,45 @@ async function call(fn) {
8464
8540
  }
8465
8541
  }
8466
8542
 
8543
+ // src/output.ts
8544
+ var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
8545
+ function printable(text2) {
8546
+ return text2.replace(CONTROL_RE, "");
8547
+ }
8548
+ function createOutput(io, json, noColor) {
8549
+ const color = !noColor && !io.env.NO_COLOR && io.env.TERM !== "dumb" && io.stderrIsTTY && !json;
8550
+ return {
8551
+ json,
8552
+ color,
8553
+ result(data, human) {
8554
+ if (json) {
8555
+ io.stdout(`${JSON.stringify(data)}
8556
+ `);
8557
+ } else if (human !== void 0) {
8558
+ io.stdout(human.endsWith("\n") ? human : `${human}
8559
+ `);
8560
+ }
8561
+ },
8562
+ info(text2) {
8563
+ io.stderr(text2.endsWith("\n") ? text2 : `${text2}
8564
+ `);
8565
+ },
8566
+ event(data) {
8567
+ if (json) io.stderr(`${JSON.stringify(data)}
8568
+ `);
8569
+ },
8570
+ bold(text2) {
8571
+ return color ? `\x1B[1m${text2}\x1B[22m` : text2;
8572
+ }
8573
+ };
8574
+ }
8575
+
8467
8576
  // src/commands/auth.ts
8468
8577
  var PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
8469
- function noBusinessMessage(consoleUrl, mayCreate) {
8470
- return mayCreate ? `You're signed in, but you don't have a workspace yet. Create one at ${consoleUrl}/new, then run \`amdahl login\` again.` : `You're signed in, but you don't have access yet. Amdahl is in beta: join the waitlist at ${consoleUrl}/new, then run \`amdahl login\` again once you're in.`;
8471
- }
8472
8578
  function tokenPreview(token) {
8473
8579
  return `${token.slice(0, 10)}...`;
8474
8580
  }
8475
- async function login(ctx) {
8581
+ async function browserSignIn(ctx) {
8476
8582
  const { io, out, flags } = ctx;
8477
8583
  const workspace = str(flags.workspace);
8478
8584
  const rawPort = str(flags.port);
@@ -8487,35 +8593,52 @@ async function login(ctx) {
8487
8593
  const endpoints = await discover(ctx.apiUrl);
8488
8594
  const pkce = createPkce();
8489
8595
  const consoleUrl = consoleUrlFor(io, ctx.apiUrl);
8596
+ let timer;
8597
+ let linkShown = false;
8490
8598
  const loopback = await startLoopback(io, {
8491
8599
  port,
8492
8600
  state: pkce.state,
8493
8601
  consoleUrl,
8494
- timeoutMs: io.loginTimeoutMs
8602
+ timeoutMs: io.loginTimeoutMs,
8603
+ // The page carries the way on; the authorize link would only sign in as the same address again.
8604
+ onSignInAgain: (reason, retry) => {
8605
+ clearTimeout(timer);
8606
+ linkShown = true;
8607
+ out.info(signInAgainLine(reason, retry));
8608
+ }
8495
8609
  });
8496
8610
  try {
8497
8611
  const url = authorizeUrl(endpoints, { redirectUri: loopback.redirectUri, pkce, workspace });
8612
+ loopback.setAuthorizeUrl(url);
8498
8613
  out.event({ event: "open_url", url });
8499
- if (flags["no-browser"]) {
8500
- out.info(`Open this URL in a browser to sign in:
8614
+ const showLink = (lead) => {
8615
+ if (linkShown) return;
8616
+ linkShown = true;
8617
+ out.info(`${lead}
8501
8618
 
8502
8619
  ${url}
8503
8620
  `);
8621
+ };
8622
+ if (flags["no-browser"]) {
8623
+ showLink("Open this URL in a browser to sign in:");
8504
8624
  } else {
8505
- out.info(`Opening your browser to sign in. If it does not open, visit:
8506
-
8507
- ${url}
8508
- `);
8509
- void io.openUrl(url);
8625
+ out.info("Opening your browser\u2026");
8626
+ void io.openUrl(url).then((opened) => {
8627
+ if (!opened) showLink("Your browser did not open. Open this link to sign in:");
8628
+ });
8629
+ timer = setTimeout(() => showLink("Finish in your browser, or open this link:"), io.browserGraceMs);
8630
+ timer.unref?.();
8510
8631
  }
8511
8632
  const callback = await loopback.result;
8633
+ clearTimeout(timer);
8634
+ linkShown = true;
8512
8635
  if (callback.kind === "no_business") {
8513
- throw new CliError("no_business", noBusinessMessage(consoleUrl, callback.mayCreate), EXIT.noWorkspace);
8636
+ throw new CliError("no_business", noBusinessMessage(consoleUrl, callback.mayCreate, callback.reason), EXIT.noWorkspace);
8514
8637
  }
8515
8638
  if (callback.kind === "error") {
8516
8639
  throw new CliError(
8517
8640
  callback.error,
8518
- `Sign-in failed: ${callback.description ?? callback.error}`,
8641
+ `Sign-in failed: ${printable(callback.description ?? callback.error)}`,
8519
8642
  callback.error === "access_denied" ? EXIT.forbidden : EXIT.general
8520
8643
  );
8521
8644
  }
@@ -8539,22 +8662,31 @@ async function login(ctx) {
8539
8662
  };
8540
8663
  ctx.config.default_profile = name;
8541
8664
  ctx.saveConfig();
8542
- out.result(
8543
- {
8544
- ok: true,
8545
- profile: name,
8546
- user: { id: me.user_id, email: me.email ?? null },
8547
- workspace: ws,
8548
- scopes: me.scopes,
8549
- api_url: ctx.apiUrl
8550
- },
8551
- `Signed in as ${me.email ?? me.user_id} to ${out.bold(ws.name)} (${ws.slug}). Profile: ${name}.`
8552
- );
8553
- return EXIT.ok;
8665
+ return { profile: name, me, workspace: ws };
8554
8666
  } finally {
8667
+ clearTimeout(timer);
8555
8668
  loopback.close();
8556
8669
  }
8557
8670
  }
8671
+ function signedInLine(ctx, { profile, me, workspace }) {
8672
+ return `Signed in as ${printable(me.email ?? me.user_id)} to ${ctx.out.bold(printable(workspace.name))} (${printable(workspace.slug)}). Profile: ${profile}.`;
8673
+ }
8674
+ async function login(ctx) {
8675
+ const signedIn = await browserSignIn(ctx);
8676
+ const { profile, me, workspace } = signedIn;
8677
+ ctx.out.result(
8678
+ {
8679
+ ok: true,
8680
+ profile,
8681
+ user: { id: me.user_id, email: me.email ?? null },
8682
+ workspace,
8683
+ scopes: me.scopes,
8684
+ api_url: ctx.apiUrl
8685
+ },
8686
+ signedInLine(ctx, signedIn)
8687
+ );
8688
+ return EXIT.ok;
8689
+ }
8558
8690
  async function logout(ctx) {
8559
8691
  const { config, out } = ctx;
8560
8692
  const names = ctx.flags.all ? Object.keys(config.profiles) : ctx.profileName && config.profiles[ctx.profileName] ? [ctx.profileName] : [];
@@ -8658,8 +8790,8 @@ var shipped_default = [
8658
8790
  var SKILL_URL = "https://docs.amdahl.ai/skills/amdahl/SKILL.md";
8659
8791
  var SKILL_TEXT = SKILL_default;
8660
8792
  var SHIPPED_SKILL_SHA256 = shipped_default;
8661
- function isShippedSkill(text) {
8662
- return SHIPPED_SKILL_SHA256.includes(createHash2("sha256").update(text).digest("hex"));
8793
+ function isShippedSkill(text2) {
8794
+ return SHIPPED_SKILL_SHA256.includes(createHash2("sha256").update(text2).digest("hex"));
8663
8795
  }
8664
8796
  var SKILL_DIRS = {
8665
8797
  "claude-code": [".claude", "skills"],
@@ -8736,14 +8868,16 @@ async function isInstalled(ctx, client) {
8736
8868
  return false;
8737
8869
  }
8738
8870
  }
8871
+ async function installClient(ctx, client, print) {
8872
+ const mcp = await installMcp(ctx, client, print);
8873
+ return { mcp, skill: installSkill(ctx, client, print) };
8874
+ }
8739
8875
  async function install(ctx, args) {
8740
8876
  const client = args[0];
8741
8877
  if (!client || args.length > 1 || !CLIENTS.includes(client)) {
8742
8878
  throw usageError(`Usage: amdahl install ${CLIENTS.join("|")} [--print] [--force]`);
8743
8879
  }
8744
- const print = ctx.flags.print === true;
8745
- const mcp = await installMcp(ctx, client, print);
8746
- const skill = installSkill(ctx, client, print);
8880
+ const { mcp, skill } = await installClient(ctx, client, ctx.flags.print === true);
8747
8881
  const human = [mcp.human.replace(/\n$/, ""), skill.human].filter(Boolean).join("\n");
8748
8882
  ctx.out.result({ ...mcp.data, skill: skill.data }, human);
8749
8883
  for (const note of [...mcp.notes, ...skill.notes]) ctx.out.info(note);
@@ -9044,6 +9178,119 @@ function withTimeout(promise, ms, file) {
9044
9178
  });
9045
9179
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
9046
9180
  }
9181
+ var WEAKEST = {
9182
+ rules: "how it follows your rules",
9183
+ ask: "the ask",
9184
+ opener: "the opener",
9185
+ fit: "how it fits the recipient",
9186
+ proof: "its proof points",
9187
+ length: "the length",
9188
+ style: "the tone",
9189
+ preference: "how it matches your preferences",
9190
+ voice: "how much it sounds like you"
9191
+ };
9192
+ var CONCERNS = {
9193
+ conflicts_with_callers_ask: "it asks for a different length or time than your ask",
9194
+ drops_ask: "it leaves out your ask",
9195
+ adds_claim: "it states something neither your draft nor your facts support",
9196
+ drops_entity: "it drops a name, link or placeholder you had",
9197
+ inflates: "it is longer than you asked for",
9198
+ no_gain: "it changed little beyond the wording",
9199
+ breaks_rules: "it breaks one of your rules"
9200
+ };
9201
+ var EVIDENCE_MISSED = {
9202
+ corpus_empty: "the workspace has none yet",
9203
+ evidence_unavailable: "this environment does not read them",
9204
+ evidence_read_failed: "the read failed, so a retry may work"
9205
+ };
9206
+ var MAX_LINES = 3;
9207
+ function list(value) {
9208
+ return Array.isArray(value) ? value.filter((v) => !!v && typeof v === "object") : null;
9209
+ }
9210
+ function record(value) {
9211
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
9212
+ }
9213
+ function text(value) {
9214
+ return typeof value === "string" ? printable(value).trim() : "";
9215
+ }
9216
+ function sentence(value) {
9217
+ return /[.!?]$/.test(value) ? value : `${value}.`;
9218
+ }
9219
+ function plural(n, one, many = `${one}s`) {
9220
+ return `${n} ${n === 1 ? one : many}`;
9221
+ }
9222
+ function readSteps(steps) {
9223
+ const reasonOf = (node) => text(steps.find((s) => s.node === node)?.reason);
9224
+ const changed = (node) => steps.some((s) => s.node === node && s.changed === true);
9225
+ const counts = new Map(
9226
+ reasonOf("w3").split(",").map((part) => part.split(":")).filter(([, n]) => /^\d+$/.test(n ?? "")).map(([code, n]) => [code, Number(n)])
9227
+ );
9228
+ const did = [];
9229
+ const repaired = counts.get("repaired") ?? 0;
9230
+ const deleted = counts.get("deleted") ?? 0;
9231
+ if (repaired && deleted) did.push(`Rewrote ${plural(repaired, "sentence")} and cut ${deleted} that nothing backed.`);
9232
+ else if (repaired) did.push(`Rewrote ${plural(repaired, "sentence")} that did not hold up.`);
9233
+ else if (deleted) did.push(`Cut ${plural(deleted, "sentence")} that nothing backed.`);
9234
+ const repair = reasonOf("w5");
9235
+ if (changed("w5") && repair.startsWith("repaired:")) {
9236
+ const weakest = WEAKEST[repair.slice("repaired:".length)];
9237
+ did.push(weakest ? `Reworked ${weakest}.` : "Reworked its weakest part.");
9238
+ }
9239
+ const flagged = [];
9240
+ const open = counts.get("open");
9241
+ if (open) flagged.push(`${plural(open, "sentence problem")} it could not fix`);
9242
+ const second = reasonOf("w6");
9243
+ if (changed("w6") && !second.startsWith("kept_before_repair")) {
9244
+ const concern = CONCERNS[second];
9245
+ flagged.push(concern ? `a second-check concern (${concern})` : "a second-check concern");
9246
+ }
9247
+ return { did: did.join(" "), flagged };
9248
+ }
9249
+ function and(parts) {
9250
+ return parts.length < 2 ? parts[0] ?? "" : `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
9251
+ }
9252
+ function whatAmdahlDid(result) {
9253
+ const steps = result.unchanged === true ? { did: "", flagged: [] } : readSteps(list(result.steps) ?? []);
9254
+ const flagged = [...steps.flagged];
9255
+ let checked = "";
9256
+ let removed = "";
9257
+ const evidence = record(result.evidence);
9258
+ const claims = list(result.unsupported_claims);
9259
+ if (evidence?.used === true) {
9260
+ const quotes = record(result.lift)?.quotes;
9261
+ const read = typeof quotes === "number" && quotes > 0 ? ` (${plural(quotes, "quote")})` : "";
9262
+ if (claims) {
9263
+ const still = claims.filter((c) => c.status !== "removed").length;
9264
+ const cut = claims.length - still;
9265
+ if (cut) removed = `Removed ${plural(cut, "claim")} your calls don't back.`;
9266
+ if (still) flagged.unshift(`${plural(still, "claim")} your calls don't back`);
9267
+ checked = `Checked every claim against your calls${read}${claims.length ? "" : " and found none they don't back"}.`;
9268
+ } else checked = `Checked the message against your calls${read}.`;
9269
+ } else if (evidence?.requested === "workspace" && EVIDENCE_MISSED[text(evidence.reason)]) {
9270
+ checked = `Skipped checking claims against your calls: ${EVIDENCE_MISSED[text(evidence.reason)]}.`;
9271
+ }
9272
+ const used = (list(record(result.context_use)?.rows) ?? []).filter((r) => r.verdict === "pass");
9273
+ const sources = [
9274
+ used.some((r) => r.bucket === "internal") ? "Amdahl's own records" : "",
9275
+ used.some((r) => r.bucket === "external") ? "what their company or market published" : ""
9276
+ ].filter(Boolean);
9277
+ const usedLine = sources.length ? `Used ${plural(sources.length, "source")}: ${and(sources)}.` : "";
9278
+ const omitted = list(result.omitted) ?? [];
9279
+ const whys = [...new Set(omitted.map((o) => text(o.why)).filter(Boolean))];
9280
+ const more = whys.length > 1 ? ` (${plural(whys.length - 1, "more reason")} in --json)` : "";
9281
+ const leftOut = omitted.length ? `Left out ${plural(omitted.length, "thing")}${whys[0] ? `: ${sentence(whys[0])}` : "."}${more}` : "";
9282
+ const lines = [
9283
+ { rank: 2, line: sentence(text(result.what_changed)) },
9284
+ { rank: 5, line: checked },
9285
+ { rank: 3, line: removed },
9286
+ { rank: 6, line: usedLine },
9287
+ { rank: 7, line: steps.did },
9288
+ { rank: 4, line: leftOut },
9289
+ { rank: 1, line: flagged.length ? `Flagged ${and(flagged)}; check before sending.` : "" }
9290
+ ].filter((l) => l.line && l.line !== ".");
9291
+ const kept = new Set([...lines].sort((a, b) => a.rank - b.rank).slice(0, MAX_LINES));
9292
+ return lines.filter((l) => kept.has(l)).map((l) => l.line);
9293
+ }
9047
9294
  async function optimize(ctx, args) {
9048
9295
  const { io, out, flags } = ctx;
9049
9296
  let files = args;
@@ -9108,6 +9355,8 @@ async function optimize(ctx, args) {
9108
9355
  if (result.unchanged === true) out.info("No rewrite beat your draft; it is returned unchanged.");
9109
9356
  else if (typeof result.summary === "string") out.info(result.summary);
9110
9357
  out.result({}, String(result.message ?? ""));
9358
+ const did = whatAmdahlDid(result);
9359
+ if (did.length) out.info(did.join("\n"));
9111
9360
  }
9112
9361
  }
9113
9362
  if (out.json) out.result(results.length === 1 ? results[0] : results);
@@ -9120,7 +9369,7 @@ import { dirname as dirname3, join as join6, relative, sep } from "path";
9120
9369
 
9121
9370
  // src/commands/skills-install.ts
9122
9371
  import { randomBytes as randomBytes3 } from "crypto";
9123
- import { chmodSync as chmodSync2, lstatSync, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
9372
+ import { chmodSync as chmodSync3, lstatSync, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
9124
9373
  import { dirname as dirname2, join as join5 } from "path";
9125
9374
  function skillDirs(targets, name) {
9126
9375
  const dirs = [];
@@ -9237,7 +9486,7 @@ function stage(target, name, archive, staged) {
9237
9486
  s.createdParent = mkdirSync3(dirname2(target), { recursive: true }) ?? void 0;
9238
9487
  refuseLinks(target);
9239
9488
  s.temp = mkdtempSync(join5(dirname2(target), `.${name}.amdahl-tmp-`));
9240
- chmodSync2(s.temp, 493);
9489
+ chmodSync3(s.temp, 493);
9241
9490
  for (const dir of archive.dirs) mkdirSync3(join5(s.temp, dir));
9242
9491
  for (const file of archive.files) writeFileSync3(join5(s.temp, file.path), file.bytes, { flag: "wx", mode: file.mode });
9243
9492
  }
@@ -9398,9 +9647,11 @@ var SHOWN = [
9398
9647
  { client: "cursor", name: "Cursor" },
9399
9648
  { client: "codex", name: "Codex" }
9400
9649
  ];
9401
- function tryItLine(name) {
9650
+ function tryItLine(name, connect = false) {
9402
9651
  const ask2 = 'say "make this email sound less like AI"';
9403
- return name === "Codex" ? `Try it: run \`codex mcp login amdahl\`, then open Codex and ${ask2}` : `Try it: open ${name} and ${ask2}`;
9652
+ if (connect && name === "Claude Code") return `Try it: in Claude Code, run /mcp once to connect, then ${ask2}`;
9653
+ if (name === "Codex") return `Try it: run \`codex mcp login amdahl\`, then open Codex and ${ask2}`;
9654
+ return `Try it: open ${name} and ${ask2}`;
9404
9655
  }
9405
9656
  function shouldSetUp(io) {
9406
9657
  const { env } = io;
@@ -9417,7 +9668,7 @@ async function signIn(ctx) {
9417
9668
  }
9418
9669
  try {
9419
9670
  const me = await call(() => apiClient(ctx, credential).cli.whoami());
9420
- return { kind: "in", email: me.email ?? me.user_id, workspace: me.workspace?.name ?? me.business_id };
9671
+ return { kind: "in", email: printable(me.email ?? me.user_id), workspace: printable(me.workspace?.name ?? me.business_id) };
9421
9672
  } catch (err) {
9422
9673
  if (!(err instanceof CliError && err.exit === EXIT.unauthenticated)) throw err;
9423
9674
  return credential.source === "profile" ? { kind: "out" } : { kind: "rejected", source: credential.source };
@@ -9425,9 +9676,13 @@ async function signIn(ctx) {
9425
9676
  }
9426
9677
  async function setup(ctx) {
9427
9678
  const { io, out } = ctx;
9679
+ const verbose = ctx.flags.verbose === true;
9428
9680
  const say = (line) => io.stdout(`${line}
9429
9681
  `);
9430
- say(`${out.bold("Amdahl")}, predictive ML for GTM`);
9682
+ const detail = (text2) => {
9683
+ if (verbose && text2) say(text2.replace(/^/gm, " "));
9684
+ };
9685
+ say(`${out.bold("Amdahl")} \xB7 predictive ML for GTM`);
9431
9686
  const found = await detectClients(ctx);
9432
9687
  const detected = SHOWN.filter((c) => found[c.client]);
9433
9688
  let who;
@@ -9448,31 +9703,33 @@ async function setup(ctx) {
9448
9703
  }
9449
9704
  }
9450
9705
  }
9451
- say(`Found on this machine: ${SHOWN.map((c) => `${c.name} ${found[c.client] ? "\u2713" : "\u2013"}`).join(" ")}`);
9452
- if (detected.length === 0) {
9453
- say("None of them found here. Tick the ones you use with Space, then press Enter.");
9454
- }
9455
9706
  const picks = await io.multiSelect(
9456
- "Set up Amdahl in:",
9707
+ "Set up in:",
9457
9708
  SHOWN.map((c) => c.name),
9458
9709
  { ticked: SHOWN.map((c) => found[c.client]), color: out.color }
9459
9710
  );
9460
9711
  if (picks.length === 0) {
9461
- say("Nothing picked, so nothing changed. To set one up later: amdahl install claude-code|codex|cursor");
9712
+ say("Nothing picked, so nothing changed.");
9462
9713
  return EXIT.ok;
9463
9714
  }
9464
9715
  who ??= await signIn(ctx);
9465
- if (who.kind === "in") say(`Signed in as ${who.email} (workspace: ${who.workspace})`);
9466
- else if (who.kind === "out") await login(ctx);
9467
- else {
9716
+ if (who.kind === "in") {
9717
+ say(`\u2713 Signed in \xB7 ${who.workspace}`);
9718
+ detail(`Signed in as ${who.email}.`);
9719
+ } else if (who.kind === "out") {
9720
+ const signedIn = await browserSignIn(ctx);
9721
+ say(`\u2713 Signed in \xB7 ${printable(signedIn.workspace.name)}`);
9722
+ detail(signedInLine(ctx, signedIn));
9723
+ } else {
9468
9724
  const name = who.source === "flag" ? "--api-key" : who.source.slice("env:".length);
9469
9725
  out.info(`Amdahl rejected the key in ${name}. Skipping the browser sign-in, because that key would still be used instead. Fix it, or unset it and run \`amdahl login\`.`);
9470
9726
  }
9471
9727
  let exit = EXIT.ok;
9472
9728
  let ready = null;
9473
9729
  for (const { client, name } of picks.flatMap((i) => SHOWN[i] ?? [])) {
9730
+ let steps;
9474
9731
  try {
9475
- await install(ctx, [client]);
9732
+ steps = await installClient(ctx, client, false);
9476
9733
  } catch (raw) {
9477
9734
  const err = raw instanceof CliError ? raw : new CliError("install_failed", `Could not set up ${name}: ${raw instanceof Error ? raw.message : String(raw)}`, EXIT.general);
9478
9735
  io.stderr(`Error: ${err.message}
@@ -9481,9 +9738,18 @@ ${err.hint ? `${err.hint}
9481
9738
  if (exit === EXIT.ok) exit = err.exit;
9482
9739
  continue;
9483
9740
  }
9484
- if (ready === null && (client !== "claude-code" || await onPath(ctx, "claude"))) ready = name;
9741
+ const action = steps.mcp.data.action;
9742
+ if (action === "printed") {
9743
+ say(`! ${name}: \`claude\` is not on your PATH. Once it is, run:
9744
+ ${String(steps.mcp.data.command)}`);
9745
+ } else {
9746
+ say(action === "unchanged" ? `\u2713 ${name} already has Amdahl` : `\u2713 Added to ${name}`);
9747
+ ready ??= { name, connect: action !== "unchanged" };
9748
+ }
9749
+ const mcpDetail = action === "printed" ? [] : [steps.mcp.human, ...steps.mcp.notes];
9750
+ detail([...mcpDetail, steps.skill.human, ...steps.skill.notes].filter(Boolean).join("\n"));
9485
9751
  }
9486
- if (ready) say(tryItLine(ready));
9752
+ if (ready) say(tryItLine(ready.name, ready.connect));
9487
9753
  return exit;
9488
9754
  }
9489
9755
 
@@ -9491,39 +9757,6 @@ ${err.hint ? `${err.hint}
9491
9757
  var import_yaml = __toESM(require_dist(), 1);
9492
9758
  import { createHash as createHash4 } from "crypto";
9493
9759
 
9494
- // src/output.ts
9495
- var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
9496
- function printable(text) {
9497
- return text.replace(CONTROL_RE, "");
9498
- }
9499
- function createOutput(io, json, noColor) {
9500
- const color = !noColor && !io.env.NO_COLOR && io.env.TERM !== "dumb" && io.stderrIsTTY && !json;
9501
- return {
9502
- json,
9503
- color,
9504
- result(data, human) {
9505
- if (json) {
9506
- io.stdout(`${JSON.stringify(data)}
9507
- `);
9508
- } else if (human !== void 0) {
9509
- io.stdout(human.endsWith("\n") ? human : `${human}
9510
- `);
9511
- }
9512
- },
9513
- info(text) {
9514
- io.stderr(text.endsWith("\n") ? text : `${text}
9515
- `);
9516
- },
9517
- event(data) {
9518
- if (json) io.stderr(`${JSON.stringify(data)}
9519
- `);
9520
- },
9521
- bold(text) {
9522
- return color ? `\x1B[1m${text}\x1B[22m` : text;
9523
- }
9524
- };
9525
- }
9526
-
9527
9760
  // src/tar-gz.ts
9528
9761
  import { inflateRawSync } from "zlib";
9529
9762
  var MAX_ARCHIVE_FILES = 200;
@@ -9598,8 +9831,8 @@ function field(header, start, length) {
9598
9831
  }
9599
9832
  }
9600
9833
  function octal(header, start, length) {
9601
- const text = header.toString("latin1", start, start + length).replace(/[\0 ]+$/, "").replace(/^ +/, "");
9602
- return /^[0-7]{1,12}$/.test(text) ? parseInt(text, 8) : null;
9834
+ const text2 = header.toString("latin1", start, start + length).replace(/[\0 ]+$/, "").replace(/^ +/, "");
9835
+ return /^[0-7]{1,12}$/.test(text2) ? parseInt(text2, 8) : null;
9603
9836
  }
9604
9837
  function checkPath(path) {
9605
9838
  const shown = JSON.stringify(path);
@@ -9725,15 +9958,15 @@ var UNREADABLE_WARNING = Object.freeze({
9725
9958
  function readWarning(raw) {
9726
9959
  if (raw === void 0 || raw === null) return void 0;
9727
9960
  if (typeof raw !== "object" || Array.isArray(raw)) return { ...UNREADABLE_WARNING };
9728
- const { result, flagged, line, text } = raw;
9961
+ const { result, flagged, line, text: text2 } = raw;
9729
9962
  if (result !== "warn" && result !== "fail") return { ...UNREADABLE_WARNING };
9730
- const sentence = typeof text === "string" ? warningText(text) : "";
9731
- if (!sentence) return { ...UNREADABLE_WARNING };
9963
+ const sentence2 = typeof text2 === "string" ? warningText(text2) : "";
9964
+ if (!sentence2) return { ...UNREADABLE_WARNING };
9732
9965
  return {
9733
9966
  result,
9734
9967
  flagged: typeof flagged === "string" && CHECK_ID_RE.test(flagged) ? flagged : null,
9735
9968
  line: typeof line === "number" && Number.isSafeInteger(line) && line > 0 ? line : null,
9736
- text: sentence
9969
+ text: sentence2
9737
9970
  };
9738
9971
  }
9739
9972
  function skillPage(site, ref) {
@@ -9929,13 +10162,13 @@ function checkDigest(body, fileUrl, sha256) {
9929
10162
  }
9930
10163
  function checkSkillMd(body, what, ref) {
9931
10164
  const bad = (why) => new CliError("invalid_skill", `${what} is not a valid skill: ${why}.`, EXIT.general);
9932
- let text;
10165
+ let text2;
9933
10166
  try {
9934
- text = new TextDecoder("utf-8", { fatal: true }).decode(body);
10167
+ text2 = new TextDecoder("utf-8", { fatal: true }).decode(body);
9935
10168
  } catch {
9936
10169
  throw bad("it is not UTF-8 text");
9937
10170
  }
9938
- const fm = FRONT_MATTER_RE.exec(text);
10171
+ const fm = FRONT_MATTER_RE.exec(text2);
9939
10172
  if (!fm) throw bad("it has no front matter");
9940
10173
  const doc = (0, import_yaml.parseDocument)(fm[1] ?? "", { uniqueKeys: true });
9941
10174
  const yamlError = doc.errors[0];
@@ -10130,7 +10363,7 @@ async function workspaceUse(ctx, args) {
10130
10363
  }
10131
10364
 
10132
10365
  // src/version.ts
10133
- var VERSION = "0.3.0";
10366
+ var VERSION = "0.3.2";
10134
10367
 
10135
10368
  // src/main.ts
10136
10369
  var COMMANDS = {
@@ -10165,7 +10398,7 @@ async function runCli(argv, io) {
10165
10398
  return EXIT.ok;
10166
10399
  }
10167
10400
  if (!parsed.command) {
10168
- if (argv.length === 0 && shouldSetUp(io)) return await setup(createCtx(io, out, parsed.flags));
10401
+ if (argv.every((a) => a === "--verbose") && shouldSetUp(io)) return await setup(createCtx(io, out, parsed.flags));
10169
10402
  io.stderr(USAGE);
10170
10403
  return EXIT.usage;
10171
10404
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amdahl/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "The amdahl command line: sign in, check setup, optimize messages, manage API keys, connect MCP clients.",
6
6
  "license": "Apache-2.0",