@amdahl/cli 0.3.4 → 0.3.6

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 +165 -66
  2. package/dist/amdahl.js +594 -311
  3. package/package.json +1 -1
package/dist/amdahl.js CHANGED
@@ -7367,10 +7367,10 @@ var require_dist = __commonJS({
7367
7367
  import { spawn } from "child_process";
7368
7368
  import { accessSync, constants, statSync } from "fs";
7369
7369
  import { homedir, hostname } from "os";
7370
- import { createInterface } from "readline";
7370
+ import { createInterface as createInterface2 } from "readline";
7371
7371
 
7372
7372
  // src/select.ts
7373
- import { emitKeypressEvents } from "readline";
7373
+ import { createInterface, emitKeypressEvents } from "readline";
7374
7374
  var HINT = "(Use the arrow keys, then Enter)";
7375
7375
  var MULTI_HINT = "\u2191\u2193 move \xB7 Space tick/untick \xB7 Enter to accept";
7376
7376
  var OFF = "\x1B[0m";
@@ -7441,7 +7441,7 @@ ${SHOW_CURSOR}`);
7441
7441
  emitKeypressEvents(input);
7442
7442
  input.setRawMode?.(true);
7443
7443
  const cols = width();
7444
- const top = [question, ...note ? wrapWords(note, cols) : []];
7444
+ const top = [question, ...note ? note.split("\n").flatMap((line) => wrapWords(line, cols)) : []];
7445
7445
  headLines = top.reduce((n, line) => n + screenLines(line, cols), 0);
7446
7446
  output.write(`${HIDE_CURSOR}${top.map((line) => `${line}
7447
7447
  `).join("")}${frame()}`);
@@ -7497,7 +7497,8 @@ function selectMenu(input, output, question, choices, options = {}) {
7497
7497
  }
7498
7498
  function multiSelectMenu(input, output, question, choices, options = {}) {
7499
7499
  const ticked = choices.map((_, i) => options.ticked?.[i] === true);
7500
- let current = Math.max(ticked.indexOf(true), 0);
7500
+ const start = Math.min(Math.max(options.initial ?? 0, 0), choices.length - 1);
7501
+ let current = ticked.includes(true) ? ticked.indexOf(true) : start;
7501
7502
  const pointer = `${marker(options)}>${OFF}`;
7502
7503
  const pad = Math.max(...choices.map((c) => c.length));
7503
7504
  const label = (i) => {
@@ -7568,6 +7569,56 @@ function listenForEnter(input, onEnter, onCancel) {
7568
7569
  }
7569
7570
  return stop;
7570
7571
  }
7572
+ var PASTE_ON = "\x1B[?2004h";
7573
+ var PASTE_OFF = "\x1B[?2004l";
7574
+ var PASTE_SETTLE_MS = 150;
7575
+ function readPaste(input, output, question) {
7576
+ output.write(`${question}
7577
+ `);
7578
+ const rl = createInterface({ input, output, terminal: true, prompt: "" });
7579
+ output.write(PASTE_ON);
7580
+ return new Promise((resolve2, reject) => {
7581
+ const lines = [];
7582
+ let pasting = false;
7583
+ let settle;
7584
+ let settled = false;
7585
+ const finish = (outcome) => {
7586
+ if (settled) return;
7587
+ settled = true;
7588
+ clearTimeout(settle);
7589
+ input.removeListener("keypress", onKeypress);
7590
+ output.write(PASTE_OFF);
7591
+ rl.close();
7592
+ if ("text" in outcome) resolve2(outcome.text);
7593
+ else reject(outcome.error);
7594
+ };
7595
+ const done = () => finish({ text: lines.join("\n").trim() });
7596
+ const onKeypress = (_text, key) => {
7597
+ if (key?.name === "paste-start") pasting = true;
7598
+ else if (key?.name === "paste-end") pasting = false;
7599
+ else if (settle && key?.name !== "return" && key?.name !== "enter") {
7600
+ clearTimeout(settle);
7601
+ settle = void 0;
7602
+ }
7603
+ };
7604
+ input.on("keypress", onKeypress);
7605
+ rl.on("line", (line) => {
7606
+ if (line.trim() || pasting) {
7607
+ lines.push(line);
7608
+ return;
7609
+ }
7610
+ if (lines.length === 0) return;
7611
+ lines.push(line);
7612
+ clearTimeout(settle);
7613
+ settle = setTimeout(done, PASTE_SETTLE_MS);
7614
+ });
7615
+ rl.on("SIGINT", () => {
7616
+ output.write("\n");
7617
+ finish({ error: new PromptCancelled() });
7618
+ });
7619
+ rl.on("close", done);
7620
+ });
7621
+ }
7571
7622
 
7572
7623
  // src/io.ts
7573
7624
  var PromptCancelled = class extends Error {
@@ -7601,7 +7652,7 @@ async function readAllStdin() {
7601
7652
  return Buffer.concat(chunks).toString("utf8");
7602
7653
  }
7603
7654
  function promptLine(question) {
7604
- const rl = createInterface({ input: process.stdin, output: process.stderr });
7655
+ const rl = createInterface2({ input: process.stdin, output: process.stderr });
7605
7656
  return new Promise((resolve2, reject) => {
7606
7657
  let settled = false;
7607
7658
  const cancel = () => {
@@ -7620,6 +7671,22 @@ function promptLine(question) {
7620
7671
  });
7621
7672
  });
7622
7673
  }
7674
+ function runOnTerminal(command, args) {
7675
+ const hold = () => {
7676
+ };
7677
+ process.on("SIGINT", hold);
7678
+ return new Promise((resolve2) => {
7679
+ let child;
7680
+ try {
7681
+ child = spawn(command, args, { stdio: "inherit" });
7682
+ } catch {
7683
+ resolve2(-1);
7684
+ return;
7685
+ }
7686
+ child.on("error", () => resolve2(-1));
7687
+ child.on("close", (code) => resolve2(code ?? -1));
7688
+ }).finally(() => process.off("SIGINT", hold));
7689
+ }
7623
7690
  function uninterruptible(fn) {
7624
7691
  const hold = () => {
7625
7692
  };
@@ -7653,6 +7720,7 @@ function realIo() {
7653
7720
  stdoutIsTTY: Boolean(process.stdout.isTTY),
7654
7721
  stderrIsTTY: Boolean(process.stderr.isTTY),
7655
7722
  readStdin: readAllStdin,
7723
+ readPaste: (question) => readPaste(process.stdin, process.stderr, question),
7656
7724
  prompt: promptLine,
7657
7725
  select: (question, choices, options) => selectMenu(process.stdin, process.stderr, question, choices, {
7658
7726
  ...options,
@@ -7667,6 +7735,7 @@ function realIo() {
7667
7735
  listenForEnter: (onEnter, onCancel) => listenForEnter(process.stdin, onEnter, onCancel),
7668
7736
  uninterruptible,
7669
7737
  run: runProcess,
7738
+ runInteractive: runOnTerminal,
7670
7739
  openUrl: async (url) => {
7671
7740
  const [cmd, args] = openerFor(process.platform, url);
7672
7741
  return (await runProcess(cmd, args)).code === 0;
@@ -7694,7 +7763,7 @@ function startedByNpx(io) {
7694
7763
  return io.env.npm_command === "exec" || io.env.npm_lifecycle_event === "npx" || /[\\/]_npx[\\/]/.test(io.scriptPath ?? "");
7695
7764
  }
7696
7765
  function setCommandName(io) {
7697
- prefix = startedByNpx(io) ? "npx amdahl" : "amdahl";
7766
+ prefix = startedByNpx(io) ? "npx -y amdahl" : "amdahl";
7698
7767
  }
7699
7768
  function amdahlCommand(rest = "") {
7700
7769
  return rest ? `${prefix} ${rest}` : prefix;
@@ -7848,7 +7917,7 @@ function parseCommandLine(argv) {
7848
7917
  const ref = second && second.includes("/") ? second : "<publisher>/<skill>";
7849
7918
  throw usageError(
7850
7919
  second === void 0 ? "`amdahl skills` needs a subcommand." : `Unknown command \`amdahl skills ${second}\`.`,
7851
- `To add a skill: npx amdahl skills add ${ref}`
7920
+ `To add a skill: npx -y amdahl skills add ${ref}`
7852
7921
  );
7853
7922
  }
7854
7923
  if (second === void 0) throw usageError(`\`amdahl ${first}\` needs a subcommand.`);
@@ -7890,12 +7959,14 @@ function parseErrorMessage(err) {
7890
7959
  var USAGE = `Usage: amdahl <command> [options]
7891
7960
 
7892
7961
  What you can do:
7893
- amdahl Set up Claude Code, Cursor or Codex. In a
7894
- terminal it asks which and signs you in.
7895
- Run by an agent or with no terminal, it
7896
- sets up that agent, or every client it
7897
- finds, and asks nothing (also: amdahl setup)
7962
+ amdahl Set up Claude Code, Cursor or Codex, or try
7963
+ Amdahl in this terminal. In a terminal it
7964
+ asks which. Run by an agent or with no
7965
+ terminal, it sets up that agent, or every
7966
+ client it finds, and asks nothing (also:
7967
+ amdahl setup)
7898
7968
  amdahl optimize draft.md Rewrite an outbound email or LinkedIn message
7969
+ (with no file, at a terminal, paste it)
7899
7970
  amdahl status Check that this workspace can optimize
7900
7971
  amdahl keys create --name ci Make an API key for a server or CI
7901
7972
  amdahl install claude-code Connect one AI client (also codex, cursor)
@@ -8836,6 +8907,8 @@ async function browserSignIn(ctx) {
8836
8907
  };
8837
8908
  ctx.config.default_profile = name;
8838
8909
  ctx.saveConfig();
8910
+ ctx.profileName = name;
8911
+ ctx.profile = ctx.config.profiles[name] ?? null;
8839
8912
  return { profile: name, me, workspace: ws };
8840
8913
  } finally {
8841
8914
  stopKeys();
@@ -8845,6 +8918,10 @@ async function browserSignIn(ctx) {
8845
8918
  function signedInLine(ctx, { profile, me, workspace }) {
8846
8919
  return `Signed in as ${printable(me.email ?? me.user_id)} to ${ctx.out.bold(printable(workspace.name))} (${printable(workspace.slug)}). Profile: ${profile}.`;
8847
8920
  }
8921
+ function loginNextLine() {
8922
+ return `Next: run \`${amdahlCommand("optimize")}\` and paste an email,
8923
+ or \`${amdahlCommand()}\` to add Amdahl to Claude Code, Cursor or Codex.`;
8924
+ }
8848
8925
  async function login(ctx) {
8849
8926
  const signedIn = await browserSignIn(ctx);
8850
8927
  const { profile, me, workspace } = signedIn;
@@ -8857,7 +8934,8 @@ async function login(ctx) {
8857
8934
  scopes: me.scopes,
8858
8935
  api_url: ctx.apiUrl
8859
8936
  },
8860
- signedInLine(ctx, signedIn)
8937
+ `${signedInLine(ctx, signedIn)}
8938
+ ${loginNextLine()}`
8861
8939
  );
8862
8940
  return EXIT.ok;
8863
8941
  }
@@ -9043,9 +9121,14 @@ async function npmGlobalPrefix(ctx) {
9043
9121
  return prefix2 || null;
9044
9122
  }
9045
9123
  async function claudeHasAmdahl(ctx, command, mcpUrl) {
9046
- const res = await ctx.io.run(command, ["mcp", "get", "amdahl"]);
9047
- return res.code === 0 && res.stdout.split("\n").some((line) => line.trim() === `URL: ${mcpUrl}`);
9124
+ let answer = claudeAnswers.get(ctx);
9125
+ if (!answer) {
9126
+ answer = ctx.io.run(command, ["mcp", "get", "amdahl"]).then((res) => res.code === 0 && res.stdout.split("\n").some((line) => line.trim() === `URL: ${mcpUrl}`));
9127
+ claudeAnswers.set(ctx, answer);
9128
+ }
9129
+ return answer;
9048
9130
  }
9131
+ var claudeAnswers = /* @__PURE__ */ new WeakMap();
9049
9132
  var CONFIG_HINT = "Fix the file, or move it away, then run the same command again.";
9050
9133
  function readJsonObject(path) {
9051
9134
  if (!existsSync3(path)) return { config: {}, existed: false };
@@ -9138,10 +9221,7 @@ function updateConfig(path, fn) {
9138
9221
  throw new CliError("write_failed", `Could not update ${path} (${reason}).`, EXIT.general);
9139
9222
  }
9140
9223
  }
9141
- async function isInstalled(ctx, client) {
9142
- const skill = skillPath(client, ctx.io.homedir);
9143
- const current = existsSync3(skill) ? readFileSync3(skill, "utf8") : null;
9144
- if (current === null || current !== SKILL_TEXT && isShippedSkill(current)) return false;
9224
+ async function hasAmdahl(ctx, client) {
9145
9225
  const mcpUrl = `${ctx.apiUrl}/mcp`;
9146
9226
  if (client === "claude-code") {
9147
9227
  if (claudeFileHasAmdahl(claudeConfigPath(ctx), mcpUrl)) return true;
@@ -9159,6 +9239,12 @@ async function isInstalled(ctx, client) {
9159
9239
  return false;
9160
9240
  }
9161
9241
  }
9242
+ async function isInstalled(ctx, client) {
9243
+ const skill = skillPath(client, ctx.io.homedir);
9244
+ const current = existsSync3(skill) ? readFileSync3(skill, "utf8") : null;
9245
+ if (current === null || current !== SKILL_TEXT && isShippedSkill(current)) return false;
9246
+ return hasAmdahl(ctx, client);
9247
+ }
9162
9248
  async function installClient(ctx, client, print) {
9163
9249
  const mcp = await installMcp(ctx, client, print);
9164
9250
  return { mcp, skill: installSkill(ctx, client, print) };
@@ -9211,6 +9297,7 @@ async function installMcp(ctx, client, print) {
9211
9297
  if (claudeFileHasAmdahl(claudeConfigPath(ctx), mcpUrl) || await claudeHasAmdahl(ctx, route.command, mcpUrl)) {
9212
9298
  return { data: { ok: true, client, action: "unchanged", command }, human: `Claude Code already has Amdahl at ${mcpUrl}; nothing changed.`, notes: [] };
9213
9299
  }
9300
+ claudeAnswers.delete(ctx);
9214
9301
  const res = await io.run(route.command, args);
9215
9302
  if (res.code !== 0) {
9216
9303
  throw new CliError("install_failed", `\`${command}\` failed: ${res.stderr.trim() || `exit ${res.code}`}`, EXIT.general);
@@ -9440,221 +9527,14 @@ async function keysRevoke(ctx, args) {
9440
9527
  }
9441
9528
 
9442
9529
  // src/commands/optimize.ts
9443
- import { readFileSync as readFileSync4 } from "fs";
9444
- var OPTIMIZE_TIMEOUT_MS = 18e4;
9445
- var CHANNELS = ["email", "linkedin"];
9446
- var EVIDENCE = ["off", "workspace"];
9447
- var ON_UNSUPPORTED = ["flag", "remove"];
9448
- function oneOf(flag, value, allowed) {
9449
- if (value === void 0) return void 0;
9450
- if (!allowed.includes(value)) throw usageError(`--${flag} must be one of: ${allowed.join(", ")}.`);
9451
- return value;
9452
- }
9453
- function readContext(path) {
9454
- let parsed;
9455
- try {
9456
- parsed = JSON.parse(readFileSync4(path, "utf8"));
9457
- } catch (err) {
9458
- throw usageError(`Could not read --context ${path}: ${err instanceof Error ? err.message : String(err)}`);
9459
- }
9460
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
9461
- throw usageError("--context must be a JSON object with rules, voice_examples and facts.");
9462
- }
9463
- return parsed;
9464
- }
9465
- function withTimeout(promise, ms, file) {
9466
- let timer;
9467
- const timeout = new Promise((_, reject) => {
9468
- timer = setTimeout(
9469
- () => reject(new CliError("timeout", `Optimizing ${file} timed out after ${ms / 1e3} s.`, EXIT.network)),
9470
- ms
9471
- );
9472
- });
9473
- return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
9474
- }
9475
- var WEAKEST = {
9476
- rules: "how it follows your rules",
9477
- ask: "the ask",
9478
- opener: "the opener",
9479
- fit: "how it fits the recipient",
9480
- proof: "its proof points",
9481
- length: "the length",
9482
- style: "the tone",
9483
- preference: "how it matches your preferences",
9484
- voice: "how much it sounds like you"
9485
- };
9486
- var CONCERNS = {
9487
- conflicts_with_callers_ask: "it asks for a different length or time than your ask",
9488
- drops_ask: "it leaves out your ask",
9489
- adds_claim: "it states something neither your draft nor your facts support",
9490
- drops_entity: "it drops a name, link or placeholder you had",
9491
- inflates: "it is longer than you asked for",
9492
- no_gain: "it changed little beyond the wording",
9493
- breaks_rules: "it breaks one of your rules"
9494
- };
9495
- var EVIDENCE_MISSED = {
9496
- corpus_empty: "the workspace has none yet",
9497
- evidence_unavailable: "this environment does not read them",
9498
- evidence_read_failed: "the read failed, so a retry may work"
9499
- };
9500
- var MAX_LINES = 3;
9501
- function list(value) {
9502
- return Array.isArray(value) ? value.filter((v) => !!v && typeof v === "object") : null;
9503
- }
9504
- function record(value) {
9505
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
9506
- }
9507
- function text(value) {
9508
- return typeof value === "string" ? printable(value).trim() : "";
9509
- }
9510
- function sentence(value) {
9511
- return /[.!?]$/.test(value) ? value : `${value}.`;
9512
- }
9513
- function plural(n, one, many = `${one}s`) {
9514
- return `${n} ${n === 1 ? one : many}`;
9515
- }
9516
- function readSteps(steps) {
9517
- const reasonOf = (node) => text(steps.find((s) => s.node === node)?.reason);
9518
- const changed = (node) => steps.some((s) => s.node === node && s.changed === true);
9519
- const counts = new Map(
9520
- reasonOf("w3").split(",").map((part) => part.split(":")).filter(([, n]) => /^\d+$/.test(n ?? "")).map(([code, n]) => [code, Number(n)])
9521
- );
9522
- const did = [];
9523
- const repaired = counts.get("repaired") ?? 0;
9524
- const deleted = counts.get("deleted") ?? 0;
9525
- if (repaired && deleted) did.push(`Rewrote ${plural(repaired, "sentence")} and cut ${deleted} that nothing backed.`);
9526
- else if (repaired) did.push(`Rewrote ${plural(repaired, "sentence")} that did not hold up.`);
9527
- else if (deleted) did.push(`Cut ${plural(deleted, "sentence")} that nothing backed.`);
9528
- const repair = reasonOf("w5");
9529
- if (changed("w5") && repair.startsWith("repaired:")) {
9530
- const weakest = WEAKEST[repair.slice("repaired:".length)];
9531
- did.push(weakest ? `Reworked ${weakest}.` : "Reworked its weakest part.");
9532
- }
9533
- const flagged = [];
9534
- const open = counts.get("open");
9535
- if (open) flagged.push(`${plural(open, "sentence problem")} it could not fix`);
9536
- const second = reasonOf("w6");
9537
- if (changed("w6") && !second.startsWith("kept_before_repair")) {
9538
- const concern = CONCERNS[second];
9539
- flagged.push(concern ? `a second-check concern (${concern})` : "a second-check concern");
9540
- }
9541
- return { did: did.join(" "), flagged };
9542
- }
9543
- function and(parts) {
9544
- return parts.length < 2 ? parts[0] ?? "" : `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
9545
- }
9546
- function whatAmdahlDid(result) {
9547
- const steps = result.unchanged === true ? { did: "", flagged: [] } : readSteps(list(result.steps) ?? []);
9548
- const flagged = [...steps.flagged];
9549
- let checked = "";
9550
- let removed = "";
9551
- const evidence = record(result.evidence);
9552
- const claims = list(result.unsupported_claims);
9553
- if (evidence?.used === true) {
9554
- const quotes = record(result.lift)?.quotes;
9555
- const read = typeof quotes === "number" && quotes > 0 ? ` (${plural(quotes, "quote")})` : "";
9556
- if (claims) {
9557
- const still = claims.filter((c) => c.status !== "removed").length;
9558
- const cut = claims.length - still;
9559
- if (cut) removed = `Removed ${plural(cut, "claim")} your calls don't back.`;
9560
- if (still) flagged.unshift(`${plural(still, "claim")} your calls don't back`);
9561
- checked = `Checked every claim against your calls${read}${claims.length ? "" : " and found none they don't back"}.`;
9562
- } else checked = `Checked the message against your calls${read}.`;
9563
- } else if (evidence?.requested === "workspace" && EVIDENCE_MISSED[text(evidence.reason)]) {
9564
- checked = `Skipped checking claims against your calls: ${EVIDENCE_MISSED[text(evidence.reason)]}.`;
9565
- }
9566
- const used = (list(record(result.context_use)?.rows) ?? []).filter((r) => r.verdict === "pass");
9567
- const sources = [
9568
- used.some((r) => r.bucket === "internal") ? "Amdahl's own records" : "",
9569
- used.some((r) => r.bucket === "external") ? "what their company or market published" : ""
9570
- ].filter(Boolean);
9571
- const usedLine = sources.length ? `Used ${plural(sources.length, "source")}: ${and(sources)}.` : "";
9572
- const omitted = list(result.omitted) ?? [];
9573
- const whys = [...new Set(omitted.map((o) => text(o.why)).filter(Boolean))];
9574
- const more = whys.length > 1 ? ` (${plural(whys.length - 1, "more reason")} in --json)` : "";
9575
- const leftOut = omitted.length ? `Left out ${plural(omitted.length, "thing")}${whys[0] ? `: ${sentence(whys[0])}` : "."}${more}` : "";
9576
- const lines = [
9577
- { rank: 2, line: sentence(text(result.what_changed)) },
9578
- { rank: 5, line: checked },
9579
- { rank: 3, line: removed },
9580
- { rank: 6, line: usedLine },
9581
- { rank: 7, line: steps.did },
9582
- { rank: 4, line: leftOut },
9583
- { rank: 1, line: flagged.length ? `Flagged ${and(flagged)}; check before sending.` : "" }
9584
- ].filter((l) => l.line && l.line !== ".");
9585
- const kept = new Set([...lines].sort((a, b) => a.rank - b.rank).slice(0, MAX_LINES));
9586
- return lines.filter((l) => kept.has(l)).map((l) => l.line);
9587
- }
9588
- async function optimize(ctx, args) {
9589
- const { io, out, flags } = ctx;
9590
- let files = args;
9591
- if (files.length === 0) {
9592
- if (io.stdinIsTTY) {
9593
- throw usageError(
9594
- "Give a file to optimize, or pipe the message on stdin.",
9595
- "Usage: amdahl optimize <file>... | - [--channel email|linkedin]"
9596
- );
9597
- }
9598
- files = ["-"];
9599
- }
9600
- if (files.filter((f) => f === "-").length > 1) throw usageError("Stdin (-) can be read only once.");
9601
- const base = {};
9602
- const channel = oneOf("channel", str(flags.channel), CHANNELS);
9603
- const evidence = oneOf("evidence", str(flags.evidence), EVIDENCE);
9604
- const onUnsupported = oneOf("on-unsupported", str(flags["on-unsupported"]), ON_UNSUPPORTED);
9605
- if (channel) base.channel = channel;
9606
- if (evidence) base.evidence = evidence;
9607
- if (onUnsupported) base.on_unsupported = onUnsupported;
9608
- if (flags.tries === true) base.include_tries = true;
9609
- const contextPath = str(flags.context);
9610
- if (contextPath) base.context = readContext(contextPath);
9611
- const inputs = [];
9612
- for (const file of files) {
9613
- let message2;
9614
- try {
9615
- message2 = file === "-" ? await io.readStdin() : readFileSync4(file, "utf8");
9616
- } catch (err) {
9617
- throw usageError(`Could not read ${file}: ${err instanceof Error ? err.message : String(err)}`);
9618
- }
9619
- if (!message2.trim()) throw usageError(`${file === "-" ? "Stdin" : file} is empty.`);
9620
- inputs.push({ file, message: message2 });
9621
- }
9622
- const credential = await resolveCredential(ctx);
9623
- const client = apiClient(ctx, credential);
9624
- const results = [];
9625
- let exit = EXIT.ok;
9626
- for (const { file, message: message2 } of inputs) {
9627
- let result;
9628
- try {
9629
- result = await withTimeout(
9630
- client.request("/api/platform/v1/messages/optimize", {
9631
- method: "POST",
9632
- body: { message: message2, ...base }
9633
- }),
9634
- OPTIMIZE_TIMEOUT_MS,
9635
- file
9636
- );
9637
- } catch (err) {
9638
- throw toCliError(err);
9639
- }
9640
- const ok = result.ok !== false;
9641
- results.push({ ok, file, result });
9642
- if (!ok) {
9643
- exit = EXIT.general;
9644
- out.info(`${file}: not optimized (${String(result.reason ?? "unknown")}): ${String(result.detail ?? "")}`);
9645
- continue;
9646
- }
9647
- if (!out.json) {
9648
- if (inputs.length > 1) out.info(`== ${file} ==`);
9649
- if (result.unchanged === true) out.info("No rewrite beat your draft; it is returned unchanged.");
9650
- else if (typeof result.summary === "string") out.info(result.summary);
9651
- out.result({}, String(result.message ?? ""));
9652
- const did = whatAmdahlDid(result);
9653
- if (did.length) out.info(did.join("\n"));
9654
- }
9655
- }
9656
- if (out.json) out.result(results.length === 1 ? results[0] : results);
9657
- return exit;
9530
+ import { readFileSync as readFileSync5 } from "fs";
9531
+
9532
+ // src/ci.ts
9533
+ var isSet = (value) => Boolean(value) && value !== "false" && value !== "0";
9534
+ function ciVariable(env) {
9535
+ const named = ["CI", "JENKINS_URL", "TF_BUILD", "CODEBUILD_BUILD_ID", "TEAMCITY_VERSION", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI"].find((name) => isSet(env[name]));
9536
+ if (named) return named;
9537
+ return isSet(env.BUILD_NUMBER) && isSet(env.JENKINS_HOME) ? "BUILD_NUMBER with JENKINS_HOME" : null;
9658
9538
  }
9659
9539
 
9660
9540
  // src/commands/skills-guide.ts
@@ -9663,7 +9543,7 @@ import { dirname as dirname3, join as join6, relative, sep } from "path";
9663
9543
 
9664
9544
  // src/commands/skills-install.ts
9665
9545
  import { randomBytes as randomBytes3 } from "crypto";
9666
- import { chmodSync as chmodSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync5, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
9546
+ import { chmodSync as chmodSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync4, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
9667
9547
  import { dirname as dirname2, join as join5 } from "path";
9668
9548
  function skillDirs(targets, name) {
9669
9549
  const dirs = [];
@@ -9710,7 +9590,7 @@ function sameFolder(dir, archive) {
9710
9590
  if (!file) return false;
9711
9591
  const full = join5(dir, path);
9712
9592
  if (process.platform !== "win32" && (lstatSync2(full).mode & 73 ? 493 : 420) !== file.mode) return false;
9713
- if (!readFileSync5(full).equals(file.bytes)) return false;
9593
+ if (!readFileSync4(full).equals(file.bytes)) return false;
9714
9594
  } else {
9715
9595
  return false;
9716
9596
  }
@@ -9755,7 +9635,7 @@ function planInstall(dirs, payload) {
9755
9635
  refuseLinks(dir, path);
9756
9636
  let current;
9757
9637
  try {
9758
- current = lstatSync2(path, { throwIfNoEntry: false }) ? readFileSync5(path) : null;
9638
+ current = lstatSync2(path, { throwIfNoEntry: false }) ? readFileSync4(path) : null;
9759
9639
  } catch (err) {
9760
9640
  throw writeFailure(path, err);
9761
9641
  }
@@ -9935,40 +9815,379 @@ function nextStep(ctx, targets, seen) {
9935
9815
  return `Open a new ${joinNames(names, "or")} chat ${where} to use it.`;
9936
9816
  }
9937
9817
 
9938
- // src/commands/setup.ts
9939
- var SHOWN = [
9940
- { client: "claude-code", name: "Claude Code" },
9941
- { client: "cursor", name: "Cursor" },
9942
- { client: "codex", name: "Codex" }
9943
- ];
9944
- var QUESTION = "Where should Amdahl work?";
9945
- var FOUND_TAG = "found on your machine";
9946
- var NONE_FOUND = "We didn't find any of these on this computer. Tick the ones you use.";
9947
- var MENU_HINT = "\u2191\u2193 move \xB7 Space tick/untick \xB7 Enter to install";
9948
- var ASK = 'say "make this email sound less like AI"';
9949
- function finishLines(names, inClaudeCode = false) {
9950
- const [only] = names;
9818
+ // src/commands/optimize.ts
9819
+ var NEW_USER = "New to Amdahl? The sign-in sets up your free workspace.";
9820
+ var OPTIMIZE_TIMEOUT_MS = 18e4;
9821
+ var CHANNELS = ["email", "linkedin"];
9822
+ var EVIDENCE = ["off", "workspace"];
9823
+ var ON_UNSUPPORTED = ["flag", "remove"];
9824
+ function oneOf(flag, value, allowed) {
9825
+ if (value === void 0) return void 0;
9826
+ if (!allowed.includes(value)) throw usageError(`--${flag} must be one of: ${allowed.join(", ")}.`);
9827
+ return value;
9828
+ }
9829
+ function readContext(path) {
9830
+ let parsed;
9831
+ try {
9832
+ parsed = JSON.parse(readFileSync5(path, "utf8"));
9833
+ } catch (err) {
9834
+ throw usageError(`Could not read --context ${path}: ${err instanceof Error ? err.message : String(err)}`);
9835
+ }
9836
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
9837
+ throw usageError("--context must be a JSON object with rules, voice_examples and facts.");
9838
+ }
9839
+ return parsed;
9840
+ }
9841
+ function withTimeout(promise, ms, file) {
9842
+ let timer;
9843
+ const timeout = new Promise((_, reject) => {
9844
+ timer = setTimeout(
9845
+ () => reject(new CliError("timeout", `Optimizing ${file} timed out after ${ms / 1e3} s.`, EXIT.network)),
9846
+ ms
9847
+ );
9848
+ });
9849
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
9850
+ }
9851
+ var WEAKEST = {
9852
+ rules: "how it follows your rules",
9853
+ ask: "the ask",
9854
+ opener: "the opener",
9855
+ fit: "how it fits the recipient",
9856
+ proof: "its proof points",
9857
+ length: "the length",
9858
+ style: "the tone",
9859
+ preference: "how it matches your preferences",
9860
+ voice: "how much it sounds like you"
9861
+ };
9862
+ var CONCERNS = {
9863
+ conflicts_with_callers_ask: "it asks for a different length or time than your ask",
9864
+ drops_ask: "it leaves out your ask",
9865
+ adds_claim: "it states something neither your draft nor your facts support",
9866
+ drops_entity: "it drops a name, link or placeholder you had",
9867
+ inflates: "it is longer than you asked for",
9868
+ no_gain: "it changed little beyond the wording",
9869
+ breaks_rules: "it breaks one of your rules"
9870
+ };
9871
+ var EVIDENCE_MISSED = {
9872
+ corpus_empty: "the workspace has none yet",
9873
+ evidence_unavailable: "this environment does not read them",
9874
+ evidence_read_failed: "the read failed, so a retry may work"
9875
+ };
9876
+ var MAX_LINES = 3;
9877
+ function list(value) {
9878
+ return Array.isArray(value) ? value.filter((v) => !!v && typeof v === "object") : null;
9879
+ }
9880
+ function record(value) {
9881
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
9882
+ }
9883
+ function text(value) {
9884
+ return typeof value === "string" ? printable(value).trim() : "";
9885
+ }
9886
+ function sentence(value) {
9887
+ return /[.!?]$/.test(value) ? value : `${value}.`;
9888
+ }
9889
+ function plural(n, one, many = `${one}s`) {
9890
+ return `${n} ${n === 1 ? one : many}`;
9891
+ }
9892
+ function readSteps(steps) {
9893
+ const reasonOf = (node) => text(steps.find((s) => s.node === node)?.reason);
9894
+ const changed = (node) => steps.some((s) => s.node === node && s.changed === true);
9895
+ const counts = new Map(
9896
+ reasonOf("w3").split(",").map((part) => part.split(":")).filter(([, n]) => /^\d+$/.test(n ?? "")).map(([code, n]) => [code, Number(n)])
9897
+ );
9898
+ const did = [];
9899
+ const repaired = counts.get("repaired") ?? 0;
9900
+ const deleted = counts.get("deleted") ?? 0;
9901
+ if (repaired && deleted) did.push(`Rewrote ${plural(repaired, "sentence")} and cut ${deleted} that nothing backed.`);
9902
+ else if (repaired) did.push(`Rewrote ${plural(repaired, "sentence")} that did not hold up.`);
9903
+ else if (deleted) did.push(`Cut ${plural(deleted, "sentence")} that nothing backed.`);
9904
+ const repair = reasonOf("w5");
9905
+ if (changed("w5") && repair.startsWith("repaired:")) {
9906
+ const weakest = WEAKEST[repair.slice("repaired:".length)];
9907
+ did.push(weakest ? `Reworked ${weakest}.` : "Reworked its weakest part.");
9908
+ }
9909
+ const flagged = [];
9910
+ const open = counts.get("open");
9911
+ if (open) flagged.push(`${plural(open, "sentence problem")} it could not fix`);
9912
+ const second = reasonOf("w6");
9913
+ if (changed("w6") && !second.startsWith("kept_before_repair")) {
9914
+ const concern = CONCERNS[second];
9915
+ flagged.push(concern ? `a second-check concern (${concern})` : "a second-check concern");
9916
+ }
9917
+ return { did: did.join(" "), flagged };
9918
+ }
9919
+ function and(parts) {
9920
+ return parts.length < 2 ? parts[0] ?? "" : `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
9921
+ }
9922
+ function whatAmdahlDid(result) {
9923
+ const steps = result.unchanged === true ? { did: "", flagged: [] } : readSteps(list(result.steps) ?? []);
9924
+ const flagged = [...steps.flagged];
9925
+ let checked = "";
9926
+ let removed = "";
9927
+ const evidence = record(result.evidence);
9928
+ const claims = list(result.unsupported_claims);
9929
+ if (evidence?.used === true) {
9930
+ const quotes = record(result.lift)?.quotes;
9931
+ const read = typeof quotes === "number" && quotes > 0 ? ` (${plural(quotes, "quote")})` : "";
9932
+ if (claims) {
9933
+ const still = claims.filter((c) => c.status !== "removed").length;
9934
+ const cut = claims.length - still;
9935
+ if (cut) removed = `Removed ${plural(cut, "claim")} your calls don't back.`;
9936
+ if (still) flagged.unshift(`${plural(still, "claim")} your calls don't back`);
9937
+ checked = `Checked every claim against your calls${read}${claims.length ? "" : " and found none they don't back"}.`;
9938
+ } else checked = `Checked the message against your calls${read}.`;
9939
+ } else if (evidence?.requested === "workspace" && EVIDENCE_MISSED[text(evidence.reason)]) {
9940
+ checked = `Skipped checking claims against your calls: ${EVIDENCE_MISSED[text(evidence.reason)]}.`;
9941
+ }
9942
+ const used = (list(record(result.context_use)?.rows) ?? []).filter((r) => r.verdict === "pass");
9943
+ const sources = [
9944
+ used.some((r) => r.bucket === "internal") ? "Amdahl's own records" : "",
9945
+ used.some((r) => r.bucket === "external") ? "what their company or market published" : ""
9946
+ ].filter(Boolean);
9947
+ const usedLine = sources.length ? `Used ${plural(sources.length, "source")}: ${and(sources)}.` : "";
9948
+ const omitted = list(result.omitted) ?? [];
9949
+ const whys = [...new Set(omitted.map((o) => text(o.why)).filter(Boolean))];
9950
+ const more = whys.length > 1 ? ` (${plural(whys.length - 1, "more reason")} in --json)` : "";
9951
+ const leftOut = omitted.length ? `Left out ${plural(omitted.length, "thing")}${whys[0] ? `: ${sentence(whys[0])}` : "."}${more}` : "";
9952
+ const lines = [
9953
+ { rank: 2, line: sentence(text(result.what_changed)) },
9954
+ { rank: 5, line: checked },
9955
+ { rank: 3, line: removed },
9956
+ { rank: 6, line: usedLine },
9957
+ { rank: 7, line: steps.did },
9958
+ { rank: 4, line: leftOut },
9959
+ { rank: 1, line: flagged.length ? `Flagged ${and(flagged)}; check before sending.` : "" }
9960
+ ].filter((l) => l.line && l.line !== ".");
9961
+ const kept = new Set([...lines].sort((a, b) => a.rank - b.rank).slice(0, MAX_LINES));
9962
+ return lines.filter((l) => kept.has(l)).map((l) => l.line);
9963
+ }
9964
+ var PASTE_PROMPT = "Paste an email or LinkedIn message, then press Enter on an empty line:";
9965
+ var VERSION_RULE = "\u2500\u2500 Amdahl's version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
9966
+ function nextTimeLine(ctx) {
9967
+ const file = amdahlCommand("optimize draft.txt");
9968
+ return ctx.io.platform === "darwin" ? `Next time: ${file},
9969
+ or pbpaste | ${amdahlCommand("optimize -")}` : `Next time: ${file}`;
9970
+ }
9971
+ function requestOptions(ctx) {
9972
+ const { flags } = ctx;
9973
+ const base = {};
9974
+ const channel = oneOf("channel", str(flags.channel), CHANNELS);
9975
+ const evidence = oneOf("evidence", str(flags.evidence), EVIDENCE);
9976
+ const onUnsupported = oneOf("on-unsupported", str(flags["on-unsupported"]), ON_UNSUPPORTED);
9977
+ if (channel) base.channel = channel;
9978
+ if (evidence) base.evidence = evidence;
9979
+ if (onUnsupported) base.on_unsupported = onUnsupported;
9980
+ if (flags.tries === true) base.include_tries = true;
9981
+ const contextPath = str(flags.context);
9982
+ if (contextPath) base.context = readContext(contextPath);
9983
+ return base;
9984
+ }
9985
+ async function optimizeOne(ctx, credential, message2, base, file) {
9986
+ try {
9987
+ return await withTimeout(
9988
+ apiClient(ctx, credential).request("/api/platform/v1/messages/optimize", {
9989
+ method: "POST",
9990
+ body: { message: message2, ...base }
9991
+ }),
9992
+ OPTIMIZE_TIMEOUT_MS,
9993
+ file
9994
+ );
9995
+ } catch (err) {
9996
+ throw toCliError(err);
9997
+ }
9998
+ }
9999
+ async function credentialOrSignIn(ctx) {
10000
+ try {
10001
+ return await resolveCredential(ctx);
10002
+ } catch (err) {
10003
+ if (!(err instanceof CliError && err.exit === EXIT.unauthenticated)) throw err;
10004
+ }
10005
+ ctx.out.info(`Sign in first, then paste your message.
10006
+ ${NEW_USER}`);
10007
+ const signedIn = await browserSignIn(ctx);
10008
+ ctx.out.info(`\u2713 Signed in \xB7 ${printable(signedIn.workspace.name)}`);
10009
+ return resolveCredential(ctx);
10010
+ }
10011
+ async function optimizePasted(ctx) {
10012
+ const { io, out } = ctx;
10013
+ const base = requestOptions(ctx);
10014
+ const credential = await credentialOrSignIn(ctx);
10015
+ const message2 = await io.readPaste(PASTE_PROMPT);
10016
+ if (!message2.trim()) {
10017
+ out.info(`Nothing pasted, so nothing was sent.
10018
+ ${nextTimeLine(ctx)}`);
10019
+ return EXIT.ok;
10020
+ }
10021
+ out.info("Amdahl is rewriting it. This can take a minute.");
10022
+ const result = await optimizeOne(ctx, credential, message2, base, "your message");
10023
+ if (result.ok === false) {
10024
+ if (out.json) out.result({ ok: false, file: "-", result });
10025
+ out.info(`Not optimized (${String(result.reason ?? "unknown")}): ${String(result.detail ?? "")}`);
10026
+ return EXIT.general;
10027
+ }
10028
+ if (out.json) {
10029
+ out.result({ ok: true, file: "-", result });
10030
+ return EXIT.ok;
10031
+ }
10032
+ out.info(VERSION_RULE);
10033
+ if (result.unchanged === true) out.info("No rewrite beat your draft; it is returned unchanged.");
10034
+ out.result({}, String(result.message ?? ""));
10035
+ const did = whatAmdahlDid(result);
10036
+ out.info([...did, nextTimeLine(ctx)].join("\n"));
10037
+ return EXIT.ok;
10038
+ }
10039
+ async function optimize(ctx, args) {
10040
+ const { io, out } = ctx;
10041
+ let files = args;
10042
+ if (files.length === 0) {
10043
+ if (io.stdinIsTTY && !ctx.nonInteractive && runningAgent(ctx) === null && !ciVariable(io.env)) return optimizePasted(ctx);
10044
+ if (io.stdinIsTTY) {
10045
+ throw usageError(
10046
+ "Give a file to optimize, or pipe the message on stdin.",
10047
+ "Usage: amdahl optimize <file>... | - [--channel email|linkedin]"
10048
+ );
10049
+ }
10050
+ files = ["-"];
10051
+ }
10052
+ if (files.filter((f) => f === "-").length > 1) throw usageError("Stdin (-) can be read only once.");
10053
+ const base = requestOptions(ctx);
10054
+ const inputs = [];
10055
+ for (const file of files) {
10056
+ let message2;
10057
+ try {
10058
+ message2 = file === "-" ? await io.readStdin() : readFileSync5(file, "utf8");
10059
+ } catch (err) {
10060
+ throw usageError(`Could not read ${file}: ${err instanceof Error ? err.message : String(err)}`);
10061
+ }
10062
+ if (!message2.trim()) throw usageError(`${file === "-" ? "Stdin" : file} is empty.`);
10063
+ inputs.push({ file, message: message2 });
10064
+ }
10065
+ const credential = await resolveCredential(ctx);
10066
+ const results = [];
10067
+ let exit = EXIT.ok;
10068
+ for (const { file, message: message2 } of inputs) {
10069
+ const result = await optimizeOne(ctx, credential, message2, base, file);
10070
+ const ok = result.ok !== false;
10071
+ results.push({ ok, file, result });
10072
+ if (!ok) {
10073
+ exit = EXIT.general;
10074
+ out.info(`${file}: not optimized (${String(result.reason ?? "unknown")}): ${String(result.detail ?? "")}`);
10075
+ continue;
10076
+ }
10077
+ if (!out.json) {
10078
+ if (inputs.length > 1) out.info(`== ${file} ==`);
10079
+ if (result.unchanged === true) out.info("No rewrite beat your draft; it is returned unchanged.");
10080
+ else if (typeof result.summary === "string") out.info(result.summary);
10081
+ out.result({}, String(result.message ?? ""));
10082
+ const did = whatAmdahlDid(result);
10083
+ if (did.length) out.info(did.join("\n"));
10084
+ }
10085
+ }
10086
+ if (out.json) out.result(results.length === 1 ? results[0] : results);
10087
+ return exit;
10088
+ }
10089
+
10090
+ // src/commands/setup.ts
10091
+ var SHOWN = [
10092
+ { client: "claude-code", name: "Claude Code" },
10093
+ { client: "cursor", name: "Cursor" },
10094
+ { client: "codex", name: "Codex" }
10095
+ ];
10096
+ var QUESTION = "Where should Amdahl work?";
10097
+ var TERMINAL = "This terminal";
10098
+ var FOUND_TAG = "found on your machine";
10099
+ var CONNECT_URL = "https://amdahl.ai/connect";
10100
+ var NONE_FOUND = `We didn't find Claude Code, Cursor or Codex on this computer.
10101
+ Using the Claude or ChatGPT app? Connect it at ${CONNECT_URL}`;
10102
+ var MENU_HINT = "\u2191\u2193 move \xB7 Space tick/untick \xB7 Enter continue";
10103
+ var ASK = '"Use Amdahl to make this sound less like AI"';
10104
+ var SIGN_IN = {
10105
+ "Claude Code": "If it's open, quit and reopen it. Type /mcp, pick amdahl, sign in.",
10106
+ // Cursor reads ~/.cursor/mcp.json at start, and lists its MCP servers under Customize > MCPs.
10107
+ Cursor: "If it's open, quit and reopen it. In Customize > MCPs, sign in to amdahl."
10108
+ };
10109
+ var TRY_IN = {
10110
+ "Claude Code": `Paste an email and say: ${ASK}`,
10111
+ Cursor: `In chat, paste an email, say: ${ASK}`,
10112
+ Codex: `In codex, paste an email, say: ${ASK}`
10113
+ };
10114
+ var CODEX_TRY = `Run codex, paste an email and say: ${ASK}`;
10115
+ var CODEX_LOGIN = "codex mcp login amdahl";
10116
+ function toolScreen(name, already) {
10117
+ return [
10118
+ already ? `\u2713 ${name} already has Amdahl.` : `\u2713 Added Amdahl to ${name}.`,
10119
+ `One step left, in ${name}:`,
10120
+ ` 1. ${SIGN_IN[name] ?? ""}`,
10121
+ ` 2. ${TRY_IN[name] ?? ""}`
10122
+ ];
10123
+ }
10124
+ function allSetLines(names) {
10125
+ const [only] = names;
10126
+ if (only === void 0) return [];
10127
+ const tryHere2 = `Want to try it here instead? Run: ${amdahlCommand("optimize")}`;
10128
+ if (names.length === 1) {
10129
+ const done = {
10130
+ "Claude Code": ["If you've already run /mcp, you're done.", "If not: quit and reopen Claude Code, type /mcp, pick amdahl, sign in."],
10131
+ Cursor: ["If you've already signed in under Customize > MCPs, you're done.", "If not: quit and reopen Cursor, then sign in to amdahl there."],
10132
+ Codex: [`If you've already run ${CODEX_LOGIN}, you're done.`, `If not, run it now in this terminal.`]
10133
+ };
10134
+ return [`Amdahl is in ${only}. You're all set.`, `Paste an email there and say: ${ASK}`, ...done[only] ?? [], tryHere2];
10135
+ }
10136
+ const step = {
10137
+ "Claude Code": "quit and reopen it, type /mcp, pick amdahl, sign in",
10138
+ Cursor: "quit and reopen it, then in Customize > MCPs, sign in to amdahl",
10139
+ Codex: `run ${CODEX_LOGIN}`
10140
+ };
10141
+ return [
10142
+ `Amdahl is in ${joinNames(names)}. You're all set.`,
10143
+ `Paste an email in ${names.length === 2 ? "either" : "any of them"} and say: ${ASK}`,
10144
+ "If you've already signed each one in, you're done. If not:",
10145
+ ...names.map((n) => ` ${n}: ${step[n] ?? ""}`),
10146
+ tryHere2
10147
+ ];
10148
+ }
10149
+ function nothingPickedLines() {
10150
+ return [
10151
+ "Nothing picked, so nothing changed.",
10152
+ `To try it here, run ${amdahlCommand()} again and tick "${TERMINAL}".`,
10153
+ `Using the Claude or ChatGPT app? Connect it at ${CONNECT_URL}`
10154
+ ];
10155
+ }
10156
+ function wrapLine(line, columns) {
10157
+ if (!columns || [...line].length <= columns) return [line];
10158
+ const lead = /^\s*(?:\d+\.\s+)?/.exec(line)?.[0] ?? "";
10159
+ const indent = " ".repeat(lead.length);
10160
+ const lines = [];
10161
+ let current = lead;
10162
+ let words = 0;
10163
+ for (const word of line.slice(lead.length).split(" ")) {
10164
+ if (words > 0 && [...`${current} ${word}`].length > columns) {
10165
+ lines.push(current);
10166
+ current = indent + word;
10167
+ } else current = words > 0 ? `${current} ${word}` : current + word;
10168
+ words++;
10169
+ }
10170
+ lines.push(current);
10171
+ return lines;
10172
+ }
10173
+ var AGENT_ASK = `paste an email and say ${ASK}`;
10174
+ function finishLines(names, inClaudeCode = false) {
10175
+ const [only] = names;
9951
10176
  if (only === void 0) return [];
9952
10177
  if (names.length === 1) {
9953
10178
  const step2 = {
9954
10179
  "Claude Code": inClaudeCode ? "Before that, restart Claude Code and run /mcp once to connect Amdahl." : "Before that, run /mcp once in Claude Code to connect Amdahl.",
9955
10180
  Cursor: "Before that, sign in when Cursor asks, the first time you use Amdahl.",
9956
- Codex: "Before that, run `codex mcp login amdahl` once to connect Amdahl."
10181
+ Codex: `Before that, run \`${CODEX_LOGIN}\` once to connect Amdahl.`
9957
10182
  };
9958
- return [`Try it: ${ASK} in ${only}.`, step2[only] ?? ""];
10183
+ return [`Try it: ${AGENT_ASK} in ${only}.`, step2[only] ?? ""];
9959
10184
  }
9960
10185
  const step = {
9961
10186
  "Claude Code": inClaudeCode ? "restart it, then run /mcp" : "run /mcp",
9962
10187
  Cursor: "sign in when it asks, on first use",
9963
- Codex: "run `codex mcp login amdahl`"
10188
+ Codex: `run \`${CODEX_LOGIN}\``
9964
10189
  };
9965
- return [`Try it: ${ASK} in any of them.`, "Before that, connect each one once:", ...names.map((n) => ` ${n}: ${step[n] ?? ""}`)];
9966
- }
9967
- var isSet = (value) => Boolean(value) && value !== "false" && value !== "0";
9968
- function ciVariable(env) {
9969
- const named = ["CI", "JENKINS_URL", "TF_BUILD", "CODEBUILD_BUILD_ID", "TEAMCITY_VERSION", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI"].find((name) => isSet(env[name]));
9970
- if (named) return named;
9971
- return isSet(env.BUILD_NUMBER) && isSet(env.JENKINS_HOME) ? "BUILD_NUMBER with JENKINS_HOME" : null;
10190
+ return [`Try it: ${AGENT_ASK} in any of them.`, "Before that, connect each one once:", ...names.map((n) => ` ${n}: ${step[n] ?? ""}`)];
9972
10191
  }
9973
10192
  function setupMode(io) {
9974
10193
  const { env } = io;
@@ -10008,35 +10227,70 @@ function reportError(io, err) {
10008
10227
  ${err.hint ? `${err.hint}
10009
10228
  ` : ""}`);
10010
10229
  }
10011
- async function installEach(ctx, clients, detail) {
10230
+ async function installEach(ctx, tools) {
10012
10231
  let exit = EXIT.ok;
10013
10232
  const ready = [];
10014
- for (const { client, name } of clients) {
10233
+ for (const tool of tools) {
10015
10234
  let steps;
10016
10235
  try {
10017
- steps = await installClient(ctx, client, false);
10236
+ steps = await installClient(ctx, tool.client, false);
10018
10237
  } catch (raw) {
10019
- const err = raw instanceof CliError ? raw : new CliError("install_failed", `Could not set up ${name}: ${raw instanceof Error ? raw.message : String(raw)}`, EXIT.general);
10238
+ const err = raw instanceof CliError ? raw : new CliError("install_failed", `Could not set up ${tool.name}: ${raw instanceof Error ? raw.message : String(raw)}`, EXIT.general);
10020
10239
  reportError(ctx.io, err);
10021
10240
  if (exit === EXIT.ok) exit = err.exit;
10022
10241
  continue;
10023
10242
  }
10024
- ctx.io.stdout(`${steps.mcp.data.action === "unchanged" ? `\u2713 ${name} already has Amdahl` : `\u2713 Added to ${name}`}
10025
- `);
10026
- ready.push(name);
10027
- detail([steps.mcp.human, ...steps.mcp.notes, steps.skill.human, ...steps.skill.notes].filter(Boolean).join("\n"));
10243
+ const detail = [steps.mcp.human, ...steps.mcp.notes, steps.skill.human, ...steps.skill.notes].filter(Boolean).join("\n");
10244
+ ready.push({ ...tool, already: steps.mcp.data.action === "unchanged", detail });
10028
10245
  }
10029
10246
  return { ready, exit };
10030
10247
  }
10248
+ function enterOrSkip(io) {
10249
+ return new Promise((resolve2) => {
10250
+ const stop = io.listenForEnter(
10251
+ () => {
10252
+ stop();
10253
+ resolve2(true);
10254
+ },
10255
+ () => resolve2(false)
10256
+ );
10257
+ });
10258
+ }
10259
+ async function codexScreen(ctx, tool, say, detail, lead) {
10260
+ say(tool.already ? "\u2713 Codex already has Amdahl." : "\u2713 Added Amdahl to Codex.");
10261
+ detail(tool.detail);
10262
+ for (const line of lead) say(line);
10263
+ if (ctx.io.platform === "win32" || !await onPath(ctx, "codex")) {
10264
+ say("One step left, in your terminal:");
10265
+ say(` 1. Run: ${CODEX_LOGIN}`);
10266
+ say(` 2. ${TRY_IN.Codex ?? ""}`);
10267
+ return;
10268
+ }
10269
+ say("Press Enter to sign Codex in to Amdahl (opens your browser).");
10270
+ if (!await enterOrSkip(ctx.io)) {
10271
+ say(`Skipped. To connect it later, run: ${CODEX_LOGIN}`);
10272
+ say(CODEX_TRY);
10273
+ return;
10274
+ }
10275
+ const code = await ctx.io.runInteractive("codex", ["mcp", "login", "amdahl"]);
10276
+ if (code === 0) say("\u2713 Codex is connected.");
10277
+ else {
10278
+ say("");
10279
+ say(`Codex didn't finish signing in. To try again, run: ${CODEX_LOGIN}`);
10280
+ }
10281
+ say(CODEX_TRY);
10282
+ }
10031
10283
  async function setup(ctx, mode) {
10032
10284
  const { io, out } = ctx;
10033
10285
  const verbose = ctx.flags.verbose === true;
10034
- const say = (line) => io.stdout(`${line}
10286
+ const say = (line) => {
10287
+ for (const part of mode === "menu" ? wrapLine(line, io.columns()) : [line]) io.stdout(`${part}
10035
10288
  `);
10289
+ };
10036
10290
  const detail = (text2) => {
10037
- if (verbose && text2) say(text2.replace(/^/gm, " "));
10291
+ if (verbose && text2) io.stdout(`${text2.replace(/^/gm, " ")}
10292
+ `);
10038
10293
  };
10039
- const inClaudeCode = Boolean(io.env.CLAUDECODE);
10040
10294
  say(`${out.bold("Amdahl")} \xB7 predictive ML for GTM`);
10041
10295
  const found = await detectClients(ctx);
10042
10296
  const detected = SHOWN.filter((c) => found[c.client]);
@@ -10049,43 +10303,43 @@ ${installLine()}
10049
10303
  `);
10050
10304
  return EXIT.general;
10051
10305
  }
10052
- const { ready, exit: exit2 } = await installEach(ctx, targets, detail);
10053
- for (const line of finishLines(ready, inClaudeCode)) say(line);
10306
+ const { ready, exit: exit2 } = await installEach(ctx, targets);
10307
+ for (const tool of ready) {
10308
+ say(tool.already ? `\u2713 ${tool.name} already has Amdahl` : `\u2713 Added to ${tool.name}`);
10309
+ detail(tool.detail);
10310
+ }
10311
+ for (const line of finishLines(ready.map((t) => t.name), Boolean(io.env.CLAUDECODE))) say(line);
10054
10312
  return exit2;
10055
10313
  }
10056
- let who;
10057
10314
  if (detected.length > 0) {
10058
- const installed = await Promise.all(detected.map((c) => isInstalled(ctx, c.client)));
10059
- if (installed.every(Boolean)) {
10060
- let unreachable = false;
10061
- try {
10062
- who = await signIn(ctx);
10063
- } catch (err) {
10064
- if (!(err instanceof CliError && err.exit === EXIT.network)) throw err;
10065
- unreachable = true;
10066
- }
10067
- if (unreachable || who?.kind === "in") {
10068
- say(`You're set up in ${joinNames(detected.map((c) => c.name))}.${unreachable ? " (Could not reach Amdahl to check your sign-in.)" : ""}`);
10069
- for (const line of finishLines(detected.map((c) => c.name), inClaudeCode)) say(line);
10070
- return EXIT.ok;
10071
- }
10072
- }
10073
- }
10074
- const picks = await io.multiSelect(
10075
- QUESTION,
10076
- SHOWN.map((c) => c.name),
10077
- {
10078
- ticked: SHOWN.map((c) => found[c.client]),
10079
- tags: SHOWN.map((c) => found[c.client] ? FOUND_TAG : void 0),
10080
- note: detected.length === 0 ? NONE_FOUND : void 0,
10081
- hint: MENU_HINT,
10082
- color: out.color
10083
- }
10084
- );
10315
+ const there = await Promise.all(detected.map((c) => hasAmdahl(ctx, c.client)));
10316
+ if (there.every(Boolean)) {
10317
+ let exit2 = EXIT.ok;
10318
+ for (const tool of detected) {
10319
+ if (await isInstalled(ctx, tool.client)) continue;
10320
+ const refreshed = await installEach(ctx, [tool]);
10321
+ for (const r of refreshed.ready) detail(r.detail);
10322
+ if (exit2 === EXIT.ok) exit2 = refreshed.exit;
10323
+ }
10324
+ for (const line of allSetLines(detected.map((c) => c.name))) say(line);
10325
+ return exit2;
10326
+ }
10327
+ }
10328
+ const rows = [...SHOWN.map((c) => c.name), TERMINAL];
10329
+ const picks = await io.multiSelect(QUESTION, rows, {
10330
+ ticked: [...SHOWN.map((c) => found[c.client]), false],
10331
+ tags: [...SHOWN.map((c) => found[c.client] ? FOUND_TAG : void 0), void 0],
10332
+ note: detected.length === 0 ? NONE_FOUND : void 0,
10333
+ // With no tool here, the marker starts on "This terminal": Space, then Enter.
10334
+ initial: detected.length === 0 ? SHOWN.length : void 0,
10335
+ hint: MENU_HINT,
10336
+ color: out.color
10337
+ });
10085
10338
  if (picks.length === 0) {
10086
- say("Nothing picked, so nothing changed.");
10339
+ for (const line of nothingPickedLines()) say(line);
10087
10340
  return EXIT.ok;
10088
10341
  }
10342
+ const here = picks.includes(SHOWN.length);
10089
10343
  let exit = EXIT.ok;
10090
10344
  const chosen = [];
10091
10345
  for (const pick of picks.flatMap((i) => SHOWN[i] ?? [])) {
@@ -10097,22 +10351,51 @@ ${installLine()}
10097
10351
  reportError(io, err);
10098
10352
  if (exit === EXIT.ok) exit = err.exit;
10099
10353
  }
10100
- if (chosen.length === 0) return exit;
10101
- who ??= await signIn(ctx);
10354
+ const done = await installEach(ctx, chosen);
10355
+ if (exit === EXIT.ok) exit = done.exit;
10356
+ let lead = [NEW_USER];
10357
+ for (const [i, tool] of done.ready.entries()) {
10358
+ if (i > 0) say("");
10359
+ if (tool.client === "codex") {
10360
+ await codexScreen(ctx, tool, say, detail, lead);
10361
+ } else {
10362
+ const [first, ...steps] = toolScreen(tool.name, tool.already);
10363
+ say(first ?? "");
10364
+ detail(tool.detail);
10365
+ for (const line of [...steps, ...lead]) say(line);
10366
+ }
10367
+ lead = [];
10368
+ }
10369
+ if (!here) return exit;
10370
+ if (done.ready.length > 0) say("");
10371
+ try {
10372
+ const code = await tryHere(ctx, say, lead);
10373
+ return exit === EXIT.ok ? code : exit;
10374
+ } catch (err) {
10375
+ if (err instanceof PromptCancelled && done.ready.length > 0) {
10376
+ throw new CliError("cancelled", `Stopped. Amdahl is still set up in ${joinNames(done.ready.map((t) => t.name))}.`, EXIT.cancelled);
10377
+ }
10378
+ throw err;
10379
+ }
10380
+ }
10381
+ async function tryHere(ctx, say, lead) {
10382
+ const { out } = ctx;
10383
+ const verbose = ctx.flags.verbose === true;
10384
+ const who = await signIn(ctx);
10102
10385
  if (who.kind === "in") {
10103
10386
  say(`\u2713 Signed in \xB7 ${who.workspace}`);
10104
- detail(`Signed in as ${who.email}.`);
10387
+ if (verbose) say(` Signed in as ${who.email}.`);
10105
10388
  } else if (who.kind === "out") {
10389
+ for (const line of lead) say(line);
10106
10390
  const signedIn = await browserSignIn(ctx);
10107
10391
  say(`\u2713 Signed in \xB7 ${printable(signedIn.workspace.name)}`);
10108
- detail(signedInLine(ctx, signedIn));
10392
+ if (verbose) say(` ${signedInLine(ctx, signedIn)}`);
10109
10393
  } else {
10110
10394
  const name = who.source === "flag" ? "--api-key" : who.source.slice("env:".length);
10111
- 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 \`${amdahlCommand("login")}\`.`);
10395
+ out.info(`Amdahl rejected the key in ${name}, so a message can't be tried here. Fix it, or unset it and run \`${amdahlCommand("login")}\`.`);
10396
+ return EXIT.unauthenticated;
10112
10397
  }
10113
- const done = await installEach(ctx, chosen, detail);
10114
- for (const line of finishLines(done.ready, inClaudeCode)) say(line);
10115
- return exit === EXIT.ok ? done.exit : exit;
10398
+ return optimizePasted(ctx);
10116
10399
  }
10117
10400
 
10118
10401
  // src/commands/skills.ts
@@ -10726,7 +11009,7 @@ async function workspaceUse(ctx, args) {
10726
11009
  }
10727
11010
 
10728
11011
  // src/version.ts
10729
- var VERSION = "0.3.4";
11012
+ var VERSION = "0.3.6";
10730
11013
 
10731
11014
  // src/main.ts
10732
11015
  var COMMANDS = {