@amdahl/cli 0.3.0 → 0.3.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 (3) hide show
  1. package/README.md +35 -16
  2. package/dist/amdahl.js +217 -99
  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 5 seconds, 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
@@ -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: 5e3,
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,6 +8091,46 @@ 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
  }
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
+ }
8090
8134
  function html(text) {
8091
8135
  return text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
8092
8136
  }
@@ -8094,6 +8138,8 @@ 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(text) {
8546
+ return text.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(text) {
8563
+ io.stderr(text.endsWith("\n") ? text : `${text}
8564
+ `);
8565
+ },
8566
+ event(data) {
8567
+ if (json) io.stderr(`${JSON.stringify(data)}
8568
+ `);
8569
+ },
8570
+ bold(text) {
8571
+ return color ? `\x1B[1m${text}\x1B[22m` : text;
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] : [];
@@ -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);
@@ -9120,7 +9254,7 @@ import { dirname as dirname3, join as join6, relative, sep } from "path";
9120
9254
 
9121
9255
  // src/commands/skills-install.ts
9122
9256
  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";
9257
+ 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
9258
  import { dirname as dirname2, join as join5 } from "path";
9125
9259
  function skillDirs(targets, name) {
9126
9260
  const dirs = [];
@@ -9237,7 +9371,7 @@ function stage(target, name, archive, staged) {
9237
9371
  s.createdParent = mkdirSync3(dirname2(target), { recursive: true }) ?? void 0;
9238
9372
  refuseLinks(target);
9239
9373
  s.temp = mkdtempSync(join5(dirname2(target), `.${name}.amdahl-tmp-`));
9240
- chmodSync2(s.temp, 493);
9374
+ chmodSync3(s.temp, 493);
9241
9375
  for (const dir of archive.dirs) mkdirSync3(join5(s.temp, dir));
9242
9376
  for (const file of archive.files) writeFileSync3(join5(s.temp, file.path), file.bytes, { flag: "wx", mode: file.mode });
9243
9377
  }
@@ -9398,9 +9532,11 @@ var SHOWN = [
9398
9532
  { client: "cursor", name: "Cursor" },
9399
9533
  { client: "codex", name: "Codex" }
9400
9534
  ];
9401
- function tryItLine(name) {
9535
+ function tryItLine(name, connect = false) {
9402
9536
  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}`;
9537
+ if (connect && name === "Claude Code") return `Try it: in Claude Code, run /mcp once to connect, then ${ask2}`;
9538
+ if (name === "Codex") return `Try it: run \`codex mcp login amdahl\`, then open Codex and ${ask2}`;
9539
+ return `Try it: open ${name} and ${ask2}`;
9404
9540
  }
9405
9541
  function shouldSetUp(io) {
9406
9542
  const { env } = io;
@@ -9417,7 +9553,7 @@ async function signIn(ctx) {
9417
9553
  }
9418
9554
  try {
9419
9555
  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 };
9556
+ return { kind: "in", email: printable(me.email ?? me.user_id), workspace: printable(me.workspace?.name ?? me.business_id) };
9421
9557
  } catch (err) {
9422
9558
  if (!(err instanceof CliError && err.exit === EXIT.unauthenticated)) throw err;
9423
9559
  return credential.source === "profile" ? { kind: "out" } : { kind: "rejected", source: credential.source };
@@ -9425,9 +9561,13 @@ async function signIn(ctx) {
9425
9561
  }
9426
9562
  async function setup(ctx) {
9427
9563
  const { io, out } = ctx;
9564
+ const verbose = ctx.flags.verbose === true;
9428
9565
  const say = (line) => io.stdout(`${line}
9429
9566
  `);
9430
- say(`${out.bold("Amdahl")}, predictive ML for GTM`);
9567
+ const detail = (text) => {
9568
+ if (verbose && text) say(text.replace(/^/gm, " "));
9569
+ };
9570
+ say(`${out.bold("Amdahl")} \xB7 predictive ML for GTM`);
9431
9571
  const found = await detectClients(ctx);
9432
9572
  const detected = SHOWN.filter((c) => found[c.client]);
9433
9573
  let who;
@@ -9448,31 +9588,33 @@ async function setup(ctx) {
9448
9588
  }
9449
9589
  }
9450
9590
  }
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
9591
  const picks = await io.multiSelect(
9456
- "Set up Amdahl in:",
9592
+ "Set up in:",
9457
9593
  SHOWN.map((c) => c.name),
9458
9594
  { ticked: SHOWN.map((c) => found[c.client]), color: out.color }
9459
9595
  );
9460
9596
  if (picks.length === 0) {
9461
- say("Nothing picked, so nothing changed. To set one up later: amdahl install claude-code|codex|cursor");
9597
+ say("Nothing picked, so nothing changed.");
9462
9598
  return EXIT.ok;
9463
9599
  }
9464
9600
  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 {
9601
+ if (who.kind === "in") {
9602
+ say(`\u2713 Signed in \xB7 ${who.workspace}`);
9603
+ detail(`Signed in as ${who.email}.`);
9604
+ } else if (who.kind === "out") {
9605
+ const signedIn = await browserSignIn(ctx);
9606
+ say(`\u2713 Signed in \xB7 ${printable(signedIn.workspace.name)}`);
9607
+ detail(signedInLine(ctx, signedIn));
9608
+ } else {
9468
9609
  const name = who.source === "flag" ? "--api-key" : who.source.slice("env:".length);
9469
9610
  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
9611
  }
9471
9612
  let exit = EXIT.ok;
9472
9613
  let ready = null;
9473
9614
  for (const { client, name } of picks.flatMap((i) => SHOWN[i] ?? [])) {
9615
+ let steps;
9474
9616
  try {
9475
- await install(ctx, [client]);
9617
+ steps = await installClient(ctx, client, false);
9476
9618
  } catch (raw) {
9477
9619
  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
9620
  io.stderr(`Error: ${err.message}
@@ -9481,9 +9623,18 @@ ${err.hint ? `${err.hint}
9481
9623
  if (exit === EXIT.ok) exit = err.exit;
9482
9624
  continue;
9483
9625
  }
9484
- if (ready === null && (client !== "claude-code" || await onPath(ctx, "claude"))) ready = name;
9626
+ const action = steps.mcp.data.action;
9627
+ if (action === "printed") {
9628
+ say(`! ${name}: \`claude\` is not on your PATH. Once it is, run:
9629
+ ${String(steps.mcp.data.command)}`);
9630
+ } else {
9631
+ say(action === "unchanged" ? `\u2713 ${name} already has Amdahl` : `\u2713 Added to ${name}`);
9632
+ ready ??= { name, connect: action !== "unchanged" };
9633
+ }
9634
+ const mcpDetail = action === "printed" ? [] : [steps.mcp.human, ...steps.mcp.notes];
9635
+ detail([...mcpDetail, steps.skill.human, ...steps.skill.notes].filter(Boolean).join("\n"));
9485
9636
  }
9486
- if (ready) say(tryItLine(ready));
9637
+ if (ready) say(tryItLine(ready.name, ready.connect));
9487
9638
  return exit;
9488
9639
  }
9489
9640
 
@@ -9491,39 +9642,6 @@ ${err.hint ? `${err.hint}
9491
9642
  var import_yaml = __toESM(require_dist(), 1);
9492
9643
  import { createHash as createHash4 } from "crypto";
9493
9644
 
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
9645
  // src/tar-gz.ts
9528
9646
  import { inflateRawSync } from "zlib";
9529
9647
  var MAX_ARCHIVE_FILES = 200;
@@ -10130,7 +10248,7 @@ async function workspaceUse(ctx, args) {
10130
10248
  }
10131
10249
 
10132
10250
  // src/version.ts
10133
- var VERSION = "0.3.0";
10251
+ var VERSION = "0.3.1";
10134
10252
 
10135
10253
  // src/main.ts
10136
10254
  var COMMANDS = {
@@ -10165,7 +10283,7 @@ async function runCli(argv, io) {
10165
10283
  return EXIT.ok;
10166
10284
  }
10167
10285
  if (!parsed.command) {
10168
- if (argv.length === 0 && shouldSetUp(io)) return await setup(createCtx(io, out, parsed.flags));
10286
+ if (argv.every((a) => a === "--verbose") && shouldSetUp(io)) return await setup(createCtx(io, out, parsed.flags));
10169
10287
  io.stderr(USAGE);
10170
10288
  return EXIT.usage;
10171
10289
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amdahl/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",