@amdahl/cli 0.3.5 → 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 +7 -4
  2. package/dist/amdahl.js +368 -362
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -140,8 +140,9 @@ Run codex, paste an email and say: "Use Amdahl to make this sound less like AI"
140
140
  Enter runs `codex mcp login amdahl` on your terminal (it prints the link and
141
141
  opens the browser). Ctrl+C there skips it and names the command to run
142
142
  later; if the sign-in does not finish, it says so and names the same
143
- command. Without `codex` on your PATH, the screen lists `codex mcp login
144
- amdahl` as step 1. With more than one tool, the screens follow one another
143
+ command. Without `codex` on your PATH, and always on Windows (where `codex`
144
+ is a `.cmd` file the CLI cannot start without a shell), the screen lists
145
+ `codex mcp login amdahl` as step 1. With more than one tool, the screens follow one another
145
146
  and the "New to Amdahl?" line is printed once. A tool that fails (a broken
146
147
  `~/.cursor/mcp.json` or `~/.claude.json`, say) prints its error, the rest
147
148
  still run, and the exit code is the first failure's. In a terminal narrower
@@ -320,8 +321,10 @@ message, or bare `amdahl` to add Amdahl to Claude Code, Cursor or Codex.
320
321
  `amdahl optimize` with no file, at a terminal, asks you to paste the message
321
322
  (as the setup's "This terminal" does) and signs you in through the browser
322
323
  first if you are not signed in ("Sign in first, then paste your message."). With stdin piped, or `-`, it reads stdin
323
- whole as before. With `AMDAHL_NO_PROMPT=1` at a terminal it still says to
324
- give a file (exit 2).
324
+ whole as before. At a terminal it still says to give a file (exit 2) when
325
+ an agent runs it (`CLAUDECODE` or `CURSOR_AGENT` set), in CI (the same
326
+ variables bare `amdahl` checks) or with `AMDAHL_NO_PROMPT=1`: none of them
327
+ can paste.
325
328
 
326
329
  On a server or in CI, skip `login` and set an API key in `AMDAHL_KEY`.
327
330
 
package/dist/amdahl.js CHANGED
@@ -9527,276 +9527,14 @@ async function keysRevoke(ctx, args) {
9527
9527
  }
9528
9528
 
9529
9529
  // src/commands/optimize.ts
9530
- import { readFileSync as readFileSync4 } from "fs";
9531
- var NEW_USER = "New to Amdahl? The sign-in sets up your free workspace.";
9532
- var OPTIMIZE_TIMEOUT_MS = 18e4;
9533
- var CHANNELS = ["email", "linkedin"];
9534
- var EVIDENCE = ["off", "workspace"];
9535
- var ON_UNSUPPORTED = ["flag", "remove"];
9536
- function oneOf(flag, value, allowed) {
9537
- if (value === void 0) return void 0;
9538
- if (!allowed.includes(value)) throw usageError(`--${flag} must be one of: ${allowed.join(", ")}.`);
9539
- return value;
9540
- }
9541
- function readContext(path) {
9542
- let parsed;
9543
- try {
9544
- parsed = JSON.parse(readFileSync4(path, "utf8"));
9545
- } catch (err) {
9546
- throw usageError(`Could not read --context ${path}: ${err instanceof Error ? err.message : String(err)}`);
9547
- }
9548
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
9549
- throw usageError("--context must be a JSON object with rules, voice_examples and facts.");
9550
- }
9551
- return parsed;
9552
- }
9553
- function withTimeout(promise, ms, file) {
9554
- let timer;
9555
- const timeout = new Promise((_, reject) => {
9556
- timer = setTimeout(
9557
- () => reject(new CliError("timeout", `Optimizing ${file} timed out after ${ms / 1e3} s.`, EXIT.network)),
9558
- ms
9559
- );
9560
- });
9561
- return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
9562
- }
9563
- var WEAKEST = {
9564
- rules: "how it follows your rules",
9565
- ask: "the ask",
9566
- opener: "the opener",
9567
- fit: "how it fits the recipient",
9568
- proof: "its proof points",
9569
- length: "the length",
9570
- style: "the tone",
9571
- preference: "how it matches your preferences",
9572
- voice: "how much it sounds like you"
9573
- };
9574
- var CONCERNS = {
9575
- conflicts_with_callers_ask: "it asks for a different length or time than your ask",
9576
- drops_ask: "it leaves out your ask",
9577
- adds_claim: "it states something neither your draft nor your facts support",
9578
- drops_entity: "it drops a name, link or placeholder you had",
9579
- inflates: "it is longer than you asked for",
9580
- no_gain: "it changed little beyond the wording",
9581
- breaks_rules: "it breaks one of your rules"
9582
- };
9583
- var EVIDENCE_MISSED = {
9584
- corpus_empty: "the workspace has none yet",
9585
- evidence_unavailable: "this environment does not read them",
9586
- evidence_read_failed: "the read failed, so a retry may work"
9587
- };
9588
- var MAX_LINES = 3;
9589
- function list(value) {
9590
- return Array.isArray(value) ? value.filter((v) => !!v && typeof v === "object") : null;
9591
- }
9592
- function record(value) {
9593
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
9594
- }
9595
- function text(value) {
9596
- return typeof value === "string" ? printable(value).trim() : "";
9597
- }
9598
- function sentence(value) {
9599
- return /[.!?]$/.test(value) ? value : `${value}.`;
9600
- }
9601
- function plural(n, one, many = `${one}s`) {
9602
- return `${n} ${n === 1 ? one : many}`;
9603
- }
9604
- function readSteps(steps) {
9605
- const reasonOf = (node) => text(steps.find((s) => s.node === node)?.reason);
9606
- const changed = (node) => steps.some((s) => s.node === node && s.changed === true);
9607
- const counts = new Map(
9608
- reasonOf("w3").split(",").map((part) => part.split(":")).filter(([, n]) => /^\d+$/.test(n ?? "")).map(([code, n]) => [code, Number(n)])
9609
- );
9610
- const did = [];
9611
- const repaired = counts.get("repaired") ?? 0;
9612
- const deleted = counts.get("deleted") ?? 0;
9613
- if (repaired && deleted) did.push(`Rewrote ${plural(repaired, "sentence")} and cut ${deleted} that nothing backed.`);
9614
- else if (repaired) did.push(`Rewrote ${plural(repaired, "sentence")} that did not hold up.`);
9615
- else if (deleted) did.push(`Cut ${plural(deleted, "sentence")} that nothing backed.`);
9616
- const repair = reasonOf("w5");
9617
- if (changed("w5") && repair.startsWith("repaired:")) {
9618
- const weakest = WEAKEST[repair.slice("repaired:".length)];
9619
- did.push(weakest ? `Reworked ${weakest}.` : "Reworked its weakest part.");
9620
- }
9621
- const flagged = [];
9622
- const open = counts.get("open");
9623
- if (open) flagged.push(`${plural(open, "sentence problem")} it could not fix`);
9624
- const second = reasonOf("w6");
9625
- if (changed("w6") && !second.startsWith("kept_before_repair")) {
9626
- const concern = CONCERNS[second];
9627
- flagged.push(concern ? `a second-check concern (${concern})` : "a second-check concern");
9628
- }
9629
- return { did: did.join(" "), flagged };
9630
- }
9631
- function and(parts) {
9632
- return parts.length < 2 ? parts[0] ?? "" : `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
9633
- }
9634
- function whatAmdahlDid(result) {
9635
- const steps = result.unchanged === true ? { did: "", flagged: [] } : readSteps(list(result.steps) ?? []);
9636
- const flagged = [...steps.flagged];
9637
- let checked = "";
9638
- let removed = "";
9639
- const evidence = record(result.evidence);
9640
- const claims = list(result.unsupported_claims);
9641
- if (evidence?.used === true) {
9642
- const quotes = record(result.lift)?.quotes;
9643
- const read = typeof quotes === "number" && quotes > 0 ? ` (${plural(quotes, "quote")})` : "";
9644
- if (claims) {
9645
- const still = claims.filter((c) => c.status !== "removed").length;
9646
- const cut = claims.length - still;
9647
- if (cut) removed = `Removed ${plural(cut, "claim")} your calls don't back.`;
9648
- if (still) flagged.unshift(`${plural(still, "claim")} your calls don't back`);
9649
- checked = `Checked every claim against your calls${read}${claims.length ? "" : " and found none they don't back"}.`;
9650
- } else checked = `Checked the message against your calls${read}.`;
9651
- } else if (evidence?.requested === "workspace" && EVIDENCE_MISSED[text(evidence.reason)]) {
9652
- checked = `Skipped checking claims against your calls: ${EVIDENCE_MISSED[text(evidence.reason)]}.`;
9653
- }
9654
- const used = (list(record(result.context_use)?.rows) ?? []).filter((r) => r.verdict === "pass");
9655
- const sources = [
9656
- used.some((r) => r.bucket === "internal") ? "Amdahl's own records" : "",
9657
- used.some((r) => r.bucket === "external") ? "what their company or market published" : ""
9658
- ].filter(Boolean);
9659
- const usedLine = sources.length ? `Used ${plural(sources.length, "source")}: ${and(sources)}.` : "";
9660
- const omitted = list(result.omitted) ?? [];
9661
- const whys = [...new Set(omitted.map((o) => text(o.why)).filter(Boolean))];
9662
- const more = whys.length > 1 ? ` (${plural(whys.length - 1, "more reason")} in --json)` : "";
9663
- const leftOut = omitted.length ? `Left out ${plural(omitted.length, "thing")}${whys[0] ? `: ${sentence(whys[0])}` : "."}${more}` : "";
9664
- const lines = [
9665
- { rank: 2, line: sentence(text(result.what_changed)) },
9666
- { rank: 5, line: checked },
9667
- { rank: 3, line: removed },
9668
- { rank: 6, line: usedLine },
9669
- { rank: 7, line: steps.did },
9670
- { rank: 4, line: leftOut },
9671
- { rank: 1, line: flagged.length ? `Flagged ${and(flagged)}; check before sending.` : "" }
9672
- ].filter((l) => l.line && l.line !== ".");
9673
- const kept = new Set([...lines].sort((a, b) => a.rank - b.rank).slice(0, MAX_LINES));
9674
- return lines.filter((l) => kept.has(l)).map((l) => l.line);
9675
- }
9676
- var PASTE_PROMPT = "Paste an email or LinkedIn message, then press Enter on an empty line:";
9677
- var VERSION_RULE = "\u2500\u2500 Amdahl's version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
9678
- function nextTimeLine(ctx) {
9679
- const file = amdahlCommand("optimize draft.txt");
9680
- return ctx.io.platform === "darwin" ? `Next time: ${file},
9681
- or pbpaste | ${amdahlCommand("optimize -")}` : `Next time: ${file}`;
9682
- }
9683
- function requestOptions(ctx) {
9684
- const { flags } = ctx;
9685
- const base = {};
9686
- const channel = oneOf("channel", str(flags.channel), CHANNELS);
9687
- const evidence = oneOf("evidence", str(flags.evidence), EVIDENCE);
9688
- const onUnsupported = oneOf("on-unsupported", str(flags["on-unsupported"]), ON_UNSUPPORTED);
9689
- if (channel) base.channel = channel;
9690
- if (evidence) base.evidence = evidence;
9691
- if (onUnsupported) base.on_unsupported = onUnsupported;
9692
- if (flags.tries === true) base.include_tries = true;
9693
- const contextPath = str(flags.context);
9694
- if (contextPath) base.context = readContext(contextPath);
9695
- return base;
9696
- }
9697
- async function optimizeOne(ctx, credential, message2, base, file) {
9698
- try {
9699
- return await withTimeout(
9700
- apiClient(ctx, credential).request("/api/platform/v1/messages/optimize", {
9701
- method: "POST",
9702
- body: { message: message2, ...base }
9703
- }),
9704
- OPTIMIZE_TIMEOUT_MS,
9705
- file
9706
- );
9707
- } catch (err) {
9708
- throw toCliError(err);
9709
- }
9710
- }
9711
- async function credentialOrSignIn(ctx) {
9712
- try {
9713
- return await resolveCredential(ctx);
9714
- } catch (err) {
9715
- if (!(err instanceof CliError && err.exit === EXIT.unauthenticated)) throw err;
9716
- }
9717
- ctx.out.info(`Sign in first, then paste your message.
9718
- ${NEW_USER}`);
9719
- const signedIn = await browserSignIn(ctx);
9720
- ctx.out.info(`\u2713 Signed in \xB7 ${printable(signedIn.workspace.name)}`);
9721
- return resolveCredential(ctx);
9722
- }
9723
- async function optimizePasted(ctx) {
9724
- const { io, out } = ctx;
9725
- const base = requestOptions(ctx);
9726
- const credential = await credentialOrSignIn(ctx);
9727
- const message2 = await io.readPaste(PASTE_PROMPT);
9728
- if (!message2.trim()) {
9729
- out.info(`Nothing pasted, so nothing was sent.
9730
- ${nextTimeLine(ctx)}`);
9731
- return EXIT.ok;
9732
- }
9733
- out.info("Amdahl is rewriting it. This can take a minute.");
9734
- const result = await optimizeOne(ctx, credential, message2, base, "your message");
9735
- if (result.ok === false) {
9736
- if (out.json) out.result({ ok: false, file: "-", result });
9737
- out.info(`Not optimized (${String(result.reason ?? "unknown")}): ${String(result.detail ?? "")}`);
9738
- return EXIT.general;
9739
- }
9740
- if (out.json) {
9741
- out.result({ ok: true, file: "-", result });
9742
- return EXIT.ok;
9743
- }
9744
- out.info(VERSION_RULE);
9745
- if (result.unchanged === true) out.info("No rewrite beat your draft; it is returned unchanged.");
9746
- out.result({}, String(result.message ?? ""));
9747
- const did = whatAmdahlDid(result);
9748
- out.info([...did, nextTimeLine(ctx)].join("\n"));
9749
- return EXIT.ok;
9750
- }
9751
- async function optimize(ctx, args) {
9752
- const { io, out } = ctx;
9753
- let files = args;
9754
- if (files.length === 0) {
9755
- if (io.stdinIsTTY && !ctx.nonInteractive) return optimizePasted(ctx);
9756
- if (io.stdinIsTTY) {
9757
- throw usageError(
9758
- "Give a file to optimize, or pipe the message on stdin.",
9759
- "Usage: amdahl optimize <file>... | - [--channel email|linkedin]"
9760
- );
9761
- }
9762
- files = ["-"];
9763
- }
9764
- if (files.filter((f) => f === "-").length > 1) throw usageError("Stdin (-) can be read only once.");
9765
- const base = requestOptions(ctx);
9766
- const inputs = [];
9767
- for (const file of files) {
9768
- let message2;
9769
- try {
9770
- message2 = file === "-" ? await io.readStdin() : readFileSync4(file, "utf8");
9771
- } catch (err) {
9772
- throw usageError(`Could not read ${file}: ${err instanceof Error ? err.message : String(err)}`);
9773
- }
9774
- if (!message2.trim()) throw usageError(`${file === "-" ? "Stdin" : file} is empty.`);
9775
- inputs.push({ file, message: message2 });
9776
- }
9777
- const credential = await resolveCredential(ctx);
9778
- const results = [];
9779
- let exit = EXIT.ok;
9780
- for (const { file, message: message2 } of inputs) {
9781
- const result = await optimizeOne(ctx, credential, message2, base, file);
9782
- const ok = result.ok !== false;
9783
- results.push({ ok, file, result });
9784
- if (!ok) {
9785
- exit = EXIT.general;
9786
- out.info(`${file}: not optimized (${String(result.reason ?? "unknown")}): ${String(result.detail ?? "")}`);
9787
- continue;
9788
- }
9789
- if (!out.json) {
9790
- if (inputs.length > 1) out.info(`== ${file} ==`);
9791
- if (result.unchanged === true) out.info("No rewrite beat your draft; it is returned unchanged.");
9792
- else if (typeof result.summary === "string") out.info(result.summary);
9793
- out.result({}, String(result.message ?? ""));
9794
- const did = whatAmdahlDid(result);
9795
- if (did.length) out.info(did.join("\n"));
9796
- }
9797
- }
9798
- if (out.json) out.result(results.length === 1 ? results[0] : results);
9799
- 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;
9800
9538
  }
9801
9539
 
9802
9540
  // src/commands/skills-guide.ts
@@ -9805,7 +9543,7 @@ import { dirname as dirname3, join as join6, relative, sep } from "path";
9805
9543
 
9806
9544
  // src/commands/skills-install.ts
9807
9545
  import { randomBytes as randomBytes3 } from "crypto";
9808
- 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";
9809
9547
  import { dirname as dirname2, join as join5 } from "path";
9810
9548
  function skillDirs(targets, name) {
9811
9549
  const dirs = [];
@@ -9852,7 +9590,7 @@ function sameFolder(dir, archive) {
9852
9590
  if (!file) return false;
9853
9591
  const full = join5(dir, path);
9854
9592
  if (process.platform !== "win32" && (lstatSync2(full).mode & 73 ? 493 : 420) !== file.mode) return false;
9855
- if (!readFileSync5(full).equals(file.bytes)) return false;
9593
+ if (!readFileSync4(full).equals(file.bytes)) return false;
9856
9594
  } else {
9857
9595
  return false;
9858
9596
  }
@@ -9897,7 +9635,7 @@ function planInstall(dirs, payload) {
9897
9635
  refuseLinks(dir, path);
9898
9636
  let current;
9899
9637
  try {
9900
- current = lstatSync2(path, { throwIfNoEntry: false }) ? readFileSync5(path) : null;
9638
+ current = lstatSync2(path, { throwIfNoEntry: false }) ? readFileSync4(path) : null;
9901
9639
  } catch (err) {
9902
9640
  throw writeFailure(path, err);
9903
9641
  }
@@ -9985,96 +9723,368 @@ function writeFiles(plan, body) {
9985
9723
  written.push(p.path);
9986
9724
  }
9987
9725
  }
9988
- function writeInstall(io, plan, name, payload) {
9989
- io.uninterruptible(() => payload.type === "archive" ? writeFolders(plan, name, payload.archive) : writeFiles(plan, payload.body));
9990
- }
9991
-
9992
- // src/commands/skills-guide.ts
9993
- function shouldGuide(ctx) {
9994
- const f = ctx.flags;
9995
- return !ctx.nonInteractive && ctx.io.stdoutIsTTY && !ctx.out.json && f.global !== true && f.agents !== true && f.yes !== true;
9996
- }
9997
- function shownPath(ctx, path) {
9998
- const under = (base) => {
9999
- const rel = relative(base, path);
10000
- return rel === "" || !rel.startsWith("..") && !rel.startsWith(sep) && !/^[a-zA-Z]:/.test(rel) ? rel : null;
10001
- };
10002
- const inCwd = under(ctx.io.cwd);
10003
- if (inCwd !== null) return inCwd ? `./${inCwd.split(sep).join("/")}` : ".";
10004
- const inHome = under(ctx.io.homedir);
10005
- if (inHome !== null) return inHome ? `~/${inHome.split(sep).join("/")}` : "~";
10006
- return path;
9726
+ function writeInstall(io, plan, name, payload) {
9727
+ io.uninterruptible(() => payload.type === "archive" ? writeFolders(plan, name, payload.archive) : writeFiles(plan, payload.body));
9728
+ }
9729
+
9730
+ // src/commands/skills-guide.ts
9731
+ function shouldGuide(ctx) {
9732
+ const f = ctx.flags;
9733
+ return !ctx.nonInteractive && ctx.io.stdoutIsTTY && !ctx.out.json && f.global !== true && f.agents !== true && f.yes !== true;
9734
+ }
9735
+ function shownPath(ctx, path) {
9736
+ const under = (base) => {
9737
+ const rel = relative(base, path);
9738
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith(sep) && !/^[a-zA-Z]:/.test(rel) ? rel : null;
9739
+ };
9740
+ const inCwd = under(ctx.io.cwd);
9741
+ if (inCwd !== null) return inCwd ? `./${inCwd.split(sep).join("/")}` : ".";
9742
+ const inHome = under(ctx.io.homedir);
9743
+ if (inHome !== null) return inHome ? `~/${inHome.split(sep).join("/")}` : "~";
9744
+ return path;
9745
+ }
9746
+ function agentsInUse(ctx) {
9747
+ const bases = [ctx.io.cwd, ctx.io.homedir];
9748
+ const seen = (dir) => bases.some((base) => existsSync4(join6(base, dir)));
9749
+ const claude = seen(".claude") || Boolean(ctx.io.env.CLAUDECODE);
9750
+ const codex = seen(".codex");
9751
+ const cursor = seen(".cursor") || Boolean(ctx.io.env.CURSOR_AGENT);
9752
+ const others = codex || cursor || seen(".agents");
9753
+ if (!claude && !others) return { claude: true, agents: true, names: ["Claude Code", "Codex", "Cursor"] };
9754
+ const names = [];
9755
+ if (claude) names.push("Claude Code");
9756
+ if (codex) names.push("Codex");
9757
+ if (cursor) names.push("Cursor");
9758
+ if (others && !codex && !cursor) names.push("other agents");
9759
+ return { claude, agents: others, names };
9760
+ }
9761
+ function runningAgent(ctx) {
9762
+ if (ctx.io.env.CLAUDECODE) return "claude";
9763
+ if (ctx.io.env.CURSOR_AGENT) return "cursor";
9764
+ return null;
9765
+ }
9766
+ function joinNames(names, word = "and") {
9767
+ if (names.length <= 1) return names[0] ?? "";
9768
+ return `${names.slice(0, -1).join(", ")} ${word} ${names[names.length - 1]}`;
9769
+ }
9770
+ function readable(slug) {
9771
+ const words = slug.replace(/-/g, " ");
9772
+ return words.charAt(0).toUpperCase() + words.slice(1);
9773
+ }
9774
+ async function guide(ctx, skill, payload) {
9775
+ try {
9776
+ return await ask(ctx, skill, payload);
9777
+ } catch (err) {
9778
+ if (err instanceof PromptCancelled) throw new CliError("cancelled", "Stopped. Nothing was written.", EXIT.cancelled);
9779
+ throw err;
9780
+ }
9781
+ }
9782
+ async function ask(ctx, skill, payload) {
9783
+ const { io, out } = ctx;
9784
+ const color = out.color;
9785
+ const used = agentsInUse(ctx);
9786
+ const count = payload.type === "archive" ? payload.archive.files.length : 1;
9787
+ const version = skill.version ? `${skill.version.startsWith("v") ? "" : "v"}${skill.version}, ` : "";
9788
+ out.info(`${readable(skill.name)}, by ${readable(skill.publisher)} (${version}${count} file${count === 1 ? "" : "s"}), for ${joinNames(used.names)}.`);
9789
+ let base = io.homedir;
9790
+ if (io.cwd !== io.homedir) {
9791
+ const here = io.cwd.startsWith(io.homedir + sep) ? `~${io.cwd.slice(io.homedir.length).split(sep).join("/")}` : io.cwd;
9792
+ const pick2 = await io.select("Install it in:", [`This project (${here})`, "Every project on this computer"], { initial: 0, color });
9793
+ base = pick2 === 1 ? io.homedir : io.cwd;
9794
+ }
9795
+ const targets = { base, claude: used.claude, agents: used.agents };
9796
+ const plan = planInstall(skillDirs(targets, skill.name), payload);
9797
+ const clashes = plan.filter((p) => p.action === "replaced");
9798
+ if (clashes.length === 0 || ctx.flags.force === true) return { kind: "install", targets, plan, force: true };
9799
+ out.info(
9800
+ [
9801
+ "",
9802
+ `A different copy of ${skill.name} is already at ${joinNames(clashes.map((p) => shownPath(ctx, p.path)))}.`,
9803
+ "Replacing it deletes that copy, with any changes you made to it."
9804
+ ].join("\n")
9805
+ );
9806
+ const pick = await io.select("Keep or replace it?", ["Keep your copy", "Replace it"], { initial: 0, color });
9807
+ return pick === 1 ? { kind: "install", targets, plan, force: true } : { kind: "kept", targets, plan };
9808
+ }
9809
+ function skillsFolders(ctx, plan) {
9810
+ return joinNames(plan.map((p) => shownPath(ctx, dirname3(p.path.endsWith("SKILL.md") ? dirname3(p.path) : p.path))));
9811
+ }
9812
+ function nextStep(ctx, targets, seen) {
9813
+ const where = targets.base === ctx.io.homedir ? "in any folder" : "in this folder";
9814
+ const names = seen.map((n) => n === "other agents" ? "other agent" : n);
9815
+ return `Open a new ${joinNames(names, "or")} chat ${where} to use it.`;
9816
+ }
9817
+
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]}`;
10007
9921
  }
10008
- function agentsInUse(ctx) {
10009
- const bases = [ctx.io.cwd, ctx.io.homedir];
10010
- const seen = (dir) => bases.some((base) => existsSync4(join6(base, dir)));
10011
- const claude = seen(".claude") || Boolean(ctx.io.env.CLAUDECODE);
10012
- const codex = seen(".codex");
10013
- const cursor = seen(".cursor") || Boolean(ctx.io.env.CURSOR_AGENT);
10014
- const others = codex || cursor || seen(".agents");
10015
- if (!claude && !others) return { claude: true, agents: true, names: ["Claude Code", "Codex", "Cursor"] };
10016
- const names = [];
10017
- if (claude) names.push("Claude Code");
10018
- if (codex) names.push("Codex");
10019
- if (cursor) names.push("Cursor");
10020
- if (others && !codex && !cursor) names.push("other agents");
10021
- return { claude, agents: others, names };
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);
10022
9963
  }
10023
- function runningAgent(ctx) {
10024
- if (ctx.io.env.CLAUDECODE) return "claude";
10025
- if (ctx.io.env.CURSOR_AGENT) return "cursor";
10026
- return null;
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}`;
10027
9970
  }
10028
- function joinNames(names, word = "and") {
10029
- if (names.length <= 1) return names[0] ?? "";
10030
- return `${names.slice(0, -1).join(", ")} ${word} ${names[names.length - 1]}`;
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;
10031
9984
  }
10032
- function readable(slug) {
10033
- const words = slug.replace(/-/g, " ");
10034
- return words.charAt(0).toUpperCase() + words.slice(1);
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
+ }
10035
9998
  }
10036
- async function guide(ctx, skill, payload) {
9999
+ async function credentialOrSignIn(ctx) {
10037
10000
  try {
10038
- return await ask(ctx, skill, payload);
10001
+ return await resolveCredential(ctx);
10039
10002
  } catch (err) {
10040
- if (err instanceof PromptCancelled) throw new CliError("cancelled", "Stopped. Nothing was written.", EXIT.cancelled);
10041
- throw err;
10003
+ if (!(err instanceof CliError && err.exit === EXIT.unauthenticated)) throw err;
10042
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);
10043
10010
  }
10044
- async function ask(ctx, skill, payload) {
10011
+ async function optimizePasted(ctx) {
10045
10012
  const { io, out } = ctx;
10046
- const color = out.color;
10047
- const used = agentsInUse(ctx);
10048
- const count = payload.type === "archive" ? payload.archive.files.length : 1;
10049
- const version = skill.version ? `${skill.version.startsWith("v") ? "" : "v"}${skill.version}, ` : "";
10050
- out.info(`${readable(skill.name)}, by ${readable(skill.publisher)} (${version}${count} file${count === 1 ? "" : "s"}), for ${joinNames(used.names)}.`);
10051
- let base = io.homedir;
10052
- if (io.cwd !== io.homedir) {
10053
- const here = io.cwd.startsWith(io.homedir + sep) ? `~${io.cwd.slice(io.homedir.length).split(sep).join("/")}` : io.cwd;
10054
- const pick2 = await io.select("Install it in:", [`This project (${here})`, "Every project on this computer"], { initial: 0, color });
10055
- base = pick2 === 1 ? io.homedir : io.cwd;
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;
10056
10020
  }
10057
- const targets = { base, claude: used.claude, agents: used.agents };
10058
- const plan = planInstall(skillDirs(targets, skill.name), payload);
10059
- const clashes = plan.filter((p) => p.action === "replaced");
10060
- if (clashes.length === 0 || ctx.flags.force === true) return { kind: "install", targets, plan, force: true };
10061
- out.info(
10062
- [
10063
- "",
10064
- `A different copy of ${skill.name} is already at ${joinNames(clashes.map((p) => shownPath(ctx, p.path)))}.`,
10065
- "Replacing it deletes that copy, with any changes you made to it."
10066
- ].join("\n")
10067
- );
10068
- const pick = await io.select("Keep or replace it?", ["Keep your copy", "Replace it"], { initial: 0, color });
10069
- return pick === 1 ? { kind: "install", targets, plan, force: true } : { kind: "kept", targets, plan };
10070
- }
10071
- function skillsFolders(ctx, plan) {
10072
- return joinNames(plan.map((p) => shownPath(ctx, dirname3(p.path.endsWith("SKILL.md") ? dirname3(p.path) : p.path))));
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;
10073
10038
  }
10074
- function nextStep(ctx, targets, seen) {
10075
- const where = targets.base === ctx.io.homedir ? "in any folder" : "in this folder";
10076
- const names = seen.map((n) => n === "other agents" ? "other agent" : n);
10077
- return `Open a new ${joinNames(names, "or")} chat ${where} to use it.`;
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;
10078
10088
  }
10079
10089
 
10080
10090
  // src/commands/setup.ts
@@ -10179,12 +10189,6 @@ function finishLines(names, inClaudeCode = false) {
10179
10189
  };
10180
10190
  return [`Try it: ${AGENT_ASK} in any of them.`, "Before that, connect each one once:", ...names.map((n) => ` ${n}: ${step[n] ?? ""}`)];
10181
10191
  }
10182
- var isSet = (value) => Boolean(value) && value !== "false" && value !== "0";
10183
- function ciVariable(env) {
10184
- const named = ["CI", "JENKINS_URL", "TF_BUILD", "CODEBUILD_BUILD_ID", "TEAMCITY_VERSION", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI"].find((name) => isSet(env[name]));
10185
- if (named) return named;
10186
- return isSet(env.BUILD_NUMBER) && isSet(env.JENKINS_HOME) ? "BUILD_NUMBER with JENKINS_HOME" : null;
10187
- }
10188
10192
  function setupMode(io) {
10189
10193
  const { env } = io;
10190
10194
  const ci = ciVariable(env);
@@ -10256,7 +10260,7 @@ async function codexScreen(ctx, tool, say, detail, lead) {
10256
10260
  say(tool.already ? "\u2713 Codex already has Amdahl." : "\u2713 Added Amdahl to Codex.");
10257
10261
  detail(tool.detail);
10258
10262
  for (const line of lead) say(line);
10259
- if (!await onPath(ctx, "codex")) {
10263
+ if (ctx.io.platform === "win32" || !await onPath(ctx, "codex")) {
10260
10264
  say("One step left, in your terminal:");
10261
10265
  say(` 1. Run: ${CODEX_LOGIN}`);
10262
10266
  say(` 2. ${TRY_IN.Codex ?? ""}`);
@@ -10310,13 +10314,15 @@ ${installLine()}
10310
10314
  if (detected.length > 0) {
10311
10315
  const there = await Promise.all(detected.map((c) => hasAmdahl(ctx, c.client)));
10312
10316
  if (there.every(Boolean)) {
10317
+ let exit2 = EXIT.ok;
10313
10318
  for (const tool of detected) {
10314
10319
  if (await isInstalled(ctx, tool.client)) continue;
10315
- const { ready } = await installEach(ctx, [tool]);
10316
- for (const r of ready) detail(r.detail);
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;
10317
10323
  }
10318
10324
  for (const line of allSetLines(detected.map((c) => c.name))) say(line);
10319
- return EXIT.ok;
10325
+ return exit2;
10320
10326
  }
10321
10327
  }
10322
10328
  const rows = [...SHOWN.map((c) => c.name), TERMINAL];
@@ -11003,7 +11009,7 @@ async function workspaceUse(ctx, args) {
11003
11009
  }
11004
11010
 
11005
11011
  // src/version.ts
11006
- var VERSION = "0.3.5";
11012
+ var VERSION = "0.3.6";
11007
11013
 
11008
11014
  // src/main.ts
11009
11015
  var COMMANDS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amdahl/cli",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
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",