@amdahl/cli 0.3.5 → 0.3.7

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 +34 -4
  2. package/dist/amdahl.js +418 -394
  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
 
@@ -500,6 +503,33 @@ nothing. `--site <origin>` sends both requests to another origin, for
500
503
  example a local copy of the site (`--site http://localhost:6782`). The
501
504
  origin must be `https`; `http` is allowed only for localhost.
502
505
 
506
+ ## Feedback link
507
+
508
+ Without `--json`, `amdahl optimize` ends each run with its run id:
509
+
510
+ ```
511
+ Run: 3f2c8a1e-...
512
+ ```
513
+
514
+ Under `--json` the run id is in `result.run_id` and nothing extra prints.
515
+
516
+ Amdahl staff can opt in to a feedback link after the run id. The link opens the
517
+ run's internal page in the Amdahl console, which only staff can see. It is off
518
+ by default. To turn it on:
519
+
520
+ - Set `"feedbackLink": true` at the top level of the config file
521
+ (`${XDG_CONFIG_HOME:-~/.config}/amdahl/config.json`, or
522
+ `%APPDATA%\amdahl\config.json` on Windows).
523
+ - Or set `AMDAHL_FEEDBACK_LINK=1` for one command or shell.
524
+ `AMDAHL_FEEDBACK_LINK=0` turns it off even when the config turns it on.
525
+
526
+ The link uses the console for your API host (see
527
+ [Targeting staging](#targeting-staging)).
528
+
529
+ Every request to the Amdahl API sends the headers
530
+ `X-Amdahl-Client: cli/<version>` and `User-Agent: amdahl-cli/<version>`, so
531
+ Amdahl can tell which runs came from the CLI. The headers are only labels. It never changes what a credential can do.
532
+
503
533
  ## Targeting staging
504
534
 
505
535
  ```sh
package/dist/amdahl.js CHANGED
@@ -8022,7 +8022,14 @@ Global options:
8022
8022
  -v, --version Show the version
8023
8023
  `;
8024
8024
 
8025
+ // src/version.ts
8026
+ var VERSION = "0.3.7";
8027
+
8025
8028
  // src/api.ts
8029
+ var CLIENT_HEADER = {
8030
+ "X-Amdahl-Client": `cli/${VERSION}`,
8031
+ "User-Agent": `amdahl-cli/${VERSION}`
8032
+ };
8026
8033
  var AmdahlApiError = class extends Error {
8027
8034
  constructor(message2, statusCode, path, responseBody, errorCode) {
8028
8035
  super(message2);
@@ -8110,7 +8117,7 @@ function createAmdahlClient(config) {
8110
8117
  const { baseUrl, token } = config;
8111
8118
  async function request(path, options = {}) {
8112
8119
  const { method = "GET", body, headers = {} } = options;
8113
- const allHeaders = { ...headers, Authorization: `Bearer ${token}` };
8120
+ const allHeaders = { ...headers, ...CLIENT_HEADER, Authorization: `Bearer ${token}` };
8114
8121
  if (body && !allHeaders["Content-Type"]) allHeaders["Content-Type"] = "application/json";
8115
8122
  const response = await fetch(`${baseUrl}${path}`, {
8116
8123
  method,
@@ -8177,7 +8184,8 @@ function loadConfig(io) {
8177
8184
  return {
8178
8185
  version: 1,
8179
8186
  default_profile: typeof parsed.default_profile === "string" ? parsed.default_profile : null,
8180
- profiles: parsed.profiles && typeof parsed.profiles === "object" ? parsed.profiles : {}
8187
+ profiles: parsed.profiles && typeof parsed.profiles === "object" ? parsed.profiles : {},
8188
+ ...typeof parsed.feedbackLink === "boolean" ? { feedbackLink: parsed.feedbackLink } : {}
8181
8189
  };
8182
8190
  } catch {
8183
8191
  return emptyConfig();
@@ -8199,6 +8207,12 @@ function saveConfig(io, config) {
8199
8207
  function selectedProfileName(io, config, flag) {
8200
8208
  return flag || io.env.AMDAHL_PROFILE || config.default_profile || null;
8201
8209
  }
8210
+ function feedbackLinkOn(io, config) {
8211
+ const env = io.env.AMDAHL_FEEDBACK_LINK?.trim().toLowerCase();
8212
+ if (env === "0" || env === "false" || env === "off") return false;
8213
+ if (env === "1" || env === "true" || env === "on") return true;
8214
+ return config.feedbackLink ?? false;
8215
+ }
8202
8216
 
8203
8217
  // src/oauth.ts
8204
8218
  import { createHash, randomBytes } from "crypto";
@@ -8213,7 +8227,7 @@ function createPkce() {
8213
8227
  }
8214
8228
  async function send(url, init) {
8215
8229
  try {
8216
- return await fetch(url, init);
8230
+ return await fetch(url, { ...init, headers: { ...init?.headers, ...CLIENT_HEADER } });
8217
8231
  } catch (err) {
8218
8232
  const message2 = err instanceof Error ? err.message : String(err);
8219
8233
  throw new CliError("network_error", `Could not reach ${new URL(url).host}: ${message2}`, EXIT.network, NETWORK_HINT);
@@ -9527,114 +9541,402 @@ async function keysRevoke(ctx, args) {
9527
9541
  }
9528
9542
 
9529
9543
  // 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;
9544
+ import { readFileSync as readFileSync5 } from "fs";
9545
+
9546
+ // src/ci.ts
9547
+ var isSet = (value) => Boolean(value) && value !== "false" && value !== "0";
9548
+ function ciVariable(env) {
9549
+ const named = ["CI", "JENKINS_URL", "TF_BUILD", "CODEBUILD_BUILD_ID", "TEAMCITY_VERSION", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI"].find((name) => isSet(env[name]));
9550
+ if (named) return named;
9551
+ return isSet(env.BUILD_NUMBER) && isSet(env.JENKINS_HOME) ? "BUILD_NUMBER with JENKINS_HOME" : null;
9540
9552
  }
9541
- function readContext(path) {
9542
- let parsed;
9553
+
9554
+ // src/commands/skills-guide.ts
9555
+ import { existsSync as existsSync4 } from "fs";
9556
+ import { dirname as dirname3, join as join6, relative, sep } from "path";
9557
+
9558
+ // src/commands/skills-install.ts
9559
+ import { randomBytes as randomBytes3 } from "crypto";
9560
+ 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";
9561
+ import { dirname as dirname2, join as join5 } from "path";
9562
+ function skillDirs(targets, name) {
9563
+ const dirs = [];
9564
+ if (targets.claude) dirs.push(join5(targets.base, ".claude", "skills", name));
9565
+ if (targets.agents) dirs.push(join5(targets.base, ".agents", "skills", name));
9566
+ return dirs;
9567
+ }
9568
+ var message = (err) => err instanceof Error ? err.message : String(err);
9569
+ function plainReason(err) {
9570
+ const code = err.code;
9571
+ if (code === "EACCES" || code === "EPERM") return "you do not have permission to write there";
9572
+ if (code === "ENOSPC") return "the disk is full";
9573
+ if (code === "EROFS") return "that disk is read-only";
9574
+ if (code === "ENOTDIR" || code === "EEXIST") return "a file is in the way";
9575
+ return message(err);
9576
+ }
9577
+ var leftBehind = (written) => written.length > 0 ? `It wrote ${written.join(" and ")} first.` : "Nothing was written.";
9578
+ function writeFailure(path, err, written = []) {
9579
+ const code = err.code;
9580
+ const left = leftBehind(written);
9581
+ const fix = code === "EACCES" || code === "EPERM" ? " Run the command from a folder you can write to." : "";
9582
+ return new CliError("write_failed", `Could not write ${path}: ${plainReason(err)}`, EXIT.general, `${left}${fix}`);
9583
+ }
9584
+ function sameFolder(dir, archive) {
9585
+ let top;
9543
9586
  try {
9544
- parsed = JSON.parse(readFileSync4(path, "utf8"));
9587
+ top = lstatSync2(dir);
9545
9588
  } catch (err) {
9546
- throw usageError(`Could not read --context ${path}: ${err instanceof Error ? err.message : String(err)}`);
9589
+ if (err.code === "ENOENT") return null;
9590
+ throw writeFailure(dir, err);
9547
9591
  }
9548
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
9549
- throw usageError("--context must be a JSON object with rules, voice_examples and facts.");
9592
+ if (!top.isDirectory()) return false;
9593
+ const want = new Map(archive.files.map((f) => [f.path, f]));
9594
+ const wantDirs = new Set(archive.dirs);
9595
+ let found = 0;
9596
+ const walk = (rel) => {
9597
+ for (const entry of readdirSync(join5(dir, rel), { withFileTypes: true })) {
9598
+ const path = rel ? `${rel}/${entry.name}` : entry.name;
9599
+ if (++found > want.size + wantDirs.size) return false;
9600
+ if (entry.isDirectory()) {
9601
+ if (!wantDirs.has(path) || !walk(path)) return false;
9602
+ } else if (entry.isFile()) {
9603
+ const file = want.get(path);
9604
+ if (!file) return false;
9605
+ const full = join5(dir, path);
9606
+ if (process.platform !== "win32" && (lstatSync2(full).mode & 73 ? 493 : 420) !== file.mode) return false;
9607
+ if (!readFileSync4(full).equals(file.bytes)) return false;
9608
+ } else {
9609
+ return false;
9610
+ }
9611
+ }
9612
+ return true;
9613
+ };
9614
+ try {
9615
+ return walk("") && found === want.size + wantDirs.size;
9616
+ } catch (err) {
9617
+ throw writeFailure(dir, err);
9550
9618
  }
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
9619
  }
9592
- function record(value) {
9593
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
9620
+ function refuseLinks(dir, file, written = []) {
9621
+ const path = file ?? dir;
9622
+ for (const part of [dirname2(dirname2(dir)), dirname2(dir), dir, ...file ? [file] : []]) {
9623
+ let stat;
9624
+ try {
9625
+ stat = lstatSync2(part);
9626
+ } catch (err) {
9627
+ if (err.code === "ENOENT") return;
9628
+ throw writeFailure(path, err, written);
9629
+ }
9630
+ const why = stat.isSymbolicLink() ? "is a link" : part === file && !stat.isFile() ? "is not a file" : "";
9631
+ if (why) {
9632
+ throw new CliError(
9633
+ "link_in_the_way",
9634
+ `Could not write ${path}: ${part} ${why}.`,
9635
+ EXIT.general,
9636
+ `${leftBehind(written)} Remove it, or install somewhere else.`
9637
+ );
9638
+ }
9639
+ }
9594
9640
  }
9595
- function text(value) {
9596
- return typeof value === "string" ? printable(value).trim() : "";
9641
+ function planInstall(dirs, payload) {
9642
+ return dirs.map((dir) => {
9643
+ if (payload.type === "archive") {
9644
+ refuseLinks(dir);
9645
+ const same = sameFolder(dir, payload.archive);
9646
+ return { path: dir, action: same === null ? "wrote" : same ? "unchanged" : "replaced" };
9647
+ }
9648
+ const path = join5(dir, "SKILL.md");
9649
+ refuseLinks(dir, path);
9650
+ let current;
9651
+ try {
9652
+ current = lstatSync2(path, { throwIfNoEntry: false }) ? readFileSync4(path) : null;
9653
+ } catch (err) {
9654
+ throw writeFailure(path, err);
9655
+ }
9656
+ return { path, action: current === null ? "wrote" : current.equals(payload.body) ? "unchanged" : "replaced" };
9657
+ });
9597
9658
  }
9598
- function sentence(value) {
9599
- return /[.!?]$/.test(value) ? value : `${value}.`;
9659
+ function refuseClashes(plan, name, force) {
9660
+ const clashes = plan.filter((p) => p.action === "replaced").map((p) => p.path);
9661
+ if (clashes.length > 0 && !force) {
9662
+ throw new CliError(
9663
+ "skill_exists",
9664
+ `A different ${name} skill is already at ${clashes.join(" and ")}.`,
9665
+ EXIT.general,
9666
+ "Nothing was written. Run again with --force to replace it."
9667
+ );
9668
+ }
9600
9669
  }
9601
- function plural(n, one, many = `${one}s`) {
9602
- return `${n} ${n === 1 ? one : many}`;
9670
+ function stage(target, name, archive, staged) {
9671
+ const s = { target, temp: "" };
9672
+ staged.push(s);
9673
+ refuseLinks(target);
9674
+ s.createdParent = mkdirSync3(dirname2(target), { recursive: true }) ?? void 0;
9675
+ refuseLinks(target);
9676
+ s.temp = mkdtempSync(join5(dirname2(target), `.${name}.amdahl-tmp-`));
9677
+ chmodSync4(s.temp, 493);
9678
+ for (const dir of archive.dirs) mkdirSync3(join5(s.temp, dir));
9679
+ for (const file of archive.files) writeFileSync3(join5(s.temp, file.path), file.bytes, { flag: "wx", mode: file.mode });
9603
9680
  }
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");
9681
+ function rollBack(staged) {
9682
+ for (const s of [...staged].reverse()) {
9683
+ try {
9684
+ if (s.swapped) {
9685
+ rmSync3(s.target, { recursive: true, force: true });
9686
+ if (s.backup) renameSync3(s.backup, s.target);
9687
+ } else if (s.backup) {
9688
+ renameSync3(s.backup, s.target);
9689
+ }
9690
+ } catch {
9691
+ }
9692
+ if (s.temp) rmSync3(s.temp, { recursive: true, force: true });
9693
+ if (s.createdParent) rmSync3(s.createdParent, { recursive: true, force: true });
9628
9694
  }
9629
- return { did: did.join(" "), flagged };
9630
9695
  }
9631
- function and(parts) {
9632
- return parts.length < 2 ? parts[0] ?? "" : `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
9696
+ function writeFolders(plan, name, archive) {
9697
+ const staged = [];
9698
+ let current = "";
9699
+ try {
9700
+ for (const p of plan) {
9701
+ if (p.action === "unchanged") continue;
9702
+ current = p.path;
9703
+ stage(p.path, name, archive, staged);
9704
+ }
9705
+ for (const s of staged) {
9706
+ current = s.target;
9707
+ if (plan.find((p) => p.path === s.target)?.action === "replaced") {
9708
+ s.backup = `${s.temp}.old`;
9709
+ renameSync3(s.target, s.backup);
9710
+ }
9711
+ renameSync3(s.temp, s.target);
9712
+ s.swapped = true;
9713
+ }
9714
+ } catch (err) {
9715
+ rollBack(staged);
9716
+ if (err instanceof CliError) throw err;
9717
+ throw writeFailure(current, err);
9718
+ }
9719
+ for (const s of staged) if (s.backup) rmSync3(s.backup, { recursive: true, force: true });
9633
9720
  }
9634
- function whatAmdahlDid(result) {
9635
- const steps = result.unchanged === true ? { did: "", flagged: [] } : readSteps(list(result.steps) ?? []);
9636
- const flagged = [...steps.flagged];
9637
- let checked = "";
9721
+ function writeFiles(plan, body) {
9722
+ const written = [];
9723
+ for (const p of plan) {
9724
+ if (p.action === "unchanged") continue;
9725
+ refuseLinks(dirname2(p.path), p.path, written);
9726
+ const temp = join5(dirname2(p.path), `.SKILL.md.amdahl-tmp-${randomBytes3(6).toString("hex")}`);
9727
+ try {
9728
+ mkdirSync3(dirname2(p.path), { recursive: true });
9729
+ refuseLinks(dirname2(p.path), p.path, written);
9730
+ writeFileSync3(temp, body, { flag: "wx", mode: 420 });
9731
+ renameSync3(temp, p.path);
9732
+ } catch (err) {
9733
+ rmSync3(temp, { force: true });
9734
+ if (err instanceof CliError) throw err;
9735
+ throw writeFailure(p.path, err, written);
9736
+ }
9737
+ written.push(p.path);
9738
+ }
9739
+ }
9740
+ function writeInstall(io, plan, name, payload) {
9741
+ io.uninterruptible(() => payload.type === "archive" ? writeFolders(plan, name, payload.archive) : writeFiles(plan, payload.body));
9742
+ }
9743
+
9744
+ // src/commands/skills-guide.ts
9745
+ function shouldGuide(ctx) {
9746
+ const f = ctx.flags;
9747
+ return !ctx.nonInteractive && ctx.io.stdoutIsTTY && !ctx.out.json && f.global !== true && f.agents !== true && f.yes !== true;
9748
+ }
9749
+ function shownPath(ctx, path) {
9750
+ const under = (base) => {
9751
+ const rel = relative(base, path);
9752
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith(sep) && !/^[a-zA-Z]:/.test(rel) ? rel : null;
9753
+ };
9754
+ const inCwd = under(ctx.io.cwd);
9755
+ if (inCwd !== null) return inCwd ? `./${inCwd.split(sep).join("/")}` : ".";
9756
+ const inHome = under(ctx.io.homedir);
9757
+ if (inHome !== null) return inHome ? `~/${inHome.split(sep).join("/")}` : "~";
9758
+ return path;
9759
+ }
9760
+ function agentsInUse(ctx) {
9761
+ const bases = [ctx.io.cwd, ctx.io.homedir];
9762
+ const seen = (dir) => bases.some((base) => existsSync4(join6(base, dir)));
9763
+ const claude = seen(".claude") || Boolean(ctx.io.env.CLAUDECODE);
9764
+ const codex = seen(".codex");
9765
+ const cursor = seen(".cursor") || Boolean(ctx.io.env.CURSOR_AGENT);
9766
+ const others = codex || cursor || seen(".agents");
9767
+ if (!claude && !others) return { claude: true, agents: true, names: ["Claude Code", "Codex", "Cursor"] };
9768
+ const names = [];
9769
+ if (claude) names.push("Claude Code");
9770
+ if (codex) names.push("Codex");
9771
+ if (cursor) names.push("Cursor");
9772
+ if (others && !codex && !cursor) names.push("other agents");
9773
+ return { claude, agents: others, names };
9774
+ }
9775
+ function runningAgent(ctx) {
9776
+ if (ctx.io.env.CLAUDECODE) return "claude";
9777
+ if (ctx.io.env.CURSOR_AGENT) return "cursor";
9778
+ return null;
9779
+ }
9780
+ function joinNames(names, word = "and") {
9781
+ if (names.length <= 1) return names[0] ?? "";
9782
+ return `${names.slice(0, -1).join(", ")} ${word} ${names[names.length - 1]}`;
9783
+ }
9784
+ function readable(slug) {
9785
+ const words = slug.replace(/-/g, " ");
9786
+ return words.charAt(0).toUpperCase() + words.slice(1);
9787
+ }
9788
+ async function guide(ctx, skill, payload) {
9789
+ try {
9790
+ return await ask(ctx, skill, payload);
9791
+ } catch (err) {
9792
+ if (err instanceof PromptCancelled) throw new CliError("cancelled", "Stopped. Nothing was written.", EXIT.cancelled);
9793
+ throw err;
9794
+ }
9795
+ }
9796
+ async function ask(ctx, skill, payload) {
9797
+ const { io, out } = ctx;
9798
+ const color = out.color;
9799
+ const used = agentsInUse(ctx);
9800
+ const count = payload.type === "archive" ? payload.archive.files.length : 1;
9801
+ const version = skill.version ? `${skill.version.startsWith("v") ? "" : "v"}${skill.version}, ` : "";
9802
+ out.info(`${readable(skill.name)}, by ${readable(skill.publisher)} (${version}${count} file${count === 1 ? "" : "s"}), for ${joinNames(used.names)}.`);
9803
+ let base = io.homedir;
9804
+ if (io.cwd !== io.homedir) {
9805
+ const here = io.cwd.startsWith(io.homedir + sep) ? `~${io.cwd.slice(io.homedir.length).split(sep).join("/")}` : io.cwd;
9806
+ const pick2 = await io.select("Install it in:", [`This project (${here})`, "Every project on this computer"], { initial: 0, color });
9807
+ base = pick2 === 1 ? io.homedir : io.cwd;
9808
+ }
9809
+ const targets = { base, claude: used.claude, agents: used.agents };
9810
+ const plan = planInstall(skillDirs(targets, skill.name), payload);
9811
+ const clashes = plan.filter((p) => p.action === "replaced");
9812
+ if (clashes.length === 0 || ctx.flags.force === true) return { kind: "install", targets, plan, force: true };
9813
+ out.info(
9814
+ [
9815
+ "",
9816
+ `A different copy of ${skill.name} is already at ${joinNames(clashes.map((p) => shownPath(ctx, p.path)))}.`,
9817
+ "Replacing it deletes that copy, with any changes you made to it."
9818
+ ].join("\n")
9819
+ );
9820
+ const pick = await io.select("Keep or replace it?", ["Keep your copy", "Replace it"], { initial: 0, color });
9821
+ return pick === 1 ? { kind: "install", targets, plan, force: true } : { kind: "kept", targets, plan };
9822
+ }
9823
+ function skillsFolders(ctx, plan) {
9824
+ return joinNames(plan.map((p) => shownPath(ctx, dirname3(p.path.endsWith("SKILL.md") ? dirname3(p.path) : p.path))));
9825
+ }
9826
+ function nextStep(ctx, targets, seen) {
9827
+ const where = targets.base === ctx.io.homedir ? "in any folder" : "in this folder";
9828
+ const names = seen.map((n) => n === "other agents" ? "other agent" : n);
9829
+ return `Open a new ${joinNames(names, "or")} chat ${where} to use it.`;
9830
+ }
9831
+
9832
+ // src/commands/optimize.ts
9833
+ var NEW_USER = "New to Amdahl? The sign-in sets up your free workspace.";
9834
+ var OPTIMIZE_TIMEOUT_MS = 18e4;
9835
+ var CHANNELS = ["email", "linkedin"];
9836
+ var EVIDENCE = ["off", "workspace"];
9837
+ var ON_UNSUPPORTED = ["flag", "remove"];
9838
+ function oneOf(flag, value, allowed) {
9839
+ if (value === void 0) return void 0;
9840
+ if (!allowed.includes(value)) throw usageError(`--${flag} must be one of: ${allowed.join(", ")}.`);
9841
+ return value;
9842
+ }
9843
+ function readContext(path) {
9844
+ let parsed;
9845
+ try {
9846
+ parsed = JSON.parse(readFileSync5(path, "utf8"));
9847
+ } catch (err) {
9848
+ throw usageError(`Could not read --context ${path}: ${err instanceof Error ? err.message : String(err)}`);
9849
+ }
9850
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
9851
+ throw usageError("--context must be a JSON object with rules, voice_examples and facts.");
9852
+ }
9853
+ return parsed;
9854
+ }
9855
+ function withTimeout(promise, ms, file) {
9856
+ let timer;
9857
+ const timeout = new Promise((_, reject) => {
9858
+ timer = setTimeout(
9859
+ () => reject(new CliError("timeout", `Optimizing ${file} timed out after ${ms / 1e3} s.`, EXIT.network)),
9860
+ ms
9861
+ );
9862
+ });
9863
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
9864
+ }
9865
+ var WEAKEST = {
9866
+ rules: "how it follows your rules",
9867
+ ask: "the ask",
9868
+ opener: "the opener",
9869
+ fit: "how it fits the recipient",
9870
+ proof: "its proof points",
9871
+ length: "the length",
9872
+ style: "the tone",
9873
+ preference: "how it matches your preferences",
9874
+ voice: "how much it sounds like you"
9875
+ };
9876
+ var CONCERNS = {
9877
+ conflicts_with_callers_ask: "it asks for a different length or time than your ask",
9878
+ drops_ask: "it leaves out your ask",
9879
+ adds_claim: "it states something neither your draft nor your facts support",
9880
+ drops_entity: "it drops a name, link or placeholder you had",
9881
+ inflates: "it is longer than you asked for",
9882
+ no_gain: "it changed little beyond the wording",
9883
+ breaks_rules: "it breaks one of your rules"
9884
+ };
9885
+ var EVIDENCE_MISSED = {
9886
+ corpus_empty: "the workspace has none yet",
9887
+ evidence_unavailable: "this environment does not read them",
9888
+ evidence_read_failed: "the read failed, so a retry may work"
9889
+ };
9890
+ var MAX_LINES = 3;
9891
+ function list(value) {
9892
+ return Array.isArray(value) ? value.filter((v) => !!v && typeof v === "object") : null;
9893
+ }
9894
+ function record(value) {
9895
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
9896
+ }
9897
+ function text(value) {
9898
+ return typeof value === "string" ? printable(value).trim() : "";
9899
+ }
9900
+ function sentence(value) {
9901
+ return /[.!?]$/.test(value) ? value : `${value}.`;
9902
+ }
9903
+ function plural(n, one, many = `${one}s`) {
9904
+ return `${n} ${n === 1 ? one : many}`;
9905
+ }
9906
+ function readSteps(steps) {
9907
+ const reasonOf = (node) => text(steps.find((s) => s.node === node)?.reason);
9908
+ const changed = (node) => steps.some((s) => s.node === node && s.changed === true);
9909
+ const counts = new Map(
9910
+ reasonOf("w3").split(",").map((part) => part.split(":")).filter(([, n]) => /^\d+$/.test(n ?? "")).map(([code, n]) => [code, Number(n)])
9911
+ );
9912
+ const did = [];
9913
+ const repaired = counts.get("repaired") ?? 0;
9914
+ const deleted = counts.get("deleted") ?? 0;
9915
+ if (repaired && deleted) did.push(`Rewrote ${plural(repaired, "sentence")} and cut ${deleted} that nothing backed.`);
9916
+ else if (repaired) did.push(`Rewrote ${plural(repaired, "sentence")} that did not hold up.`);
9917
+ else if (deleted) did.push(`Cut ${plural(deleted, "sentence")} that nothing backed.`);
9918
+ const repair = reasonOf("w5");
9919
+ if (changed("w5") && repair.startsWith("repaired:")) {
9920
+ const weakest = WEAKEST[repair.slice("repaired:".length)];
9921
+ did.push(weakest ? `Reworked ${weakest}.` : "Reworked its weakest part.");
9922
+ }
9923
+ const flagged = [];
9924
+ const open = counts.get("open");
9925
+ if (open) flagged.push(`${plural(open, "sentence problem")} it could not fix`);
9926
+ const second = reasonOf("w6");
9927
+ if (changed("w6") && !second.startsWith("kept_before_repair")) {
9928
+ const concern = CONCERNS[second];
9929
+ flagged.push(concern ? `a second-check concern (${concern})` : "a second-check concern");
9930
+ }
9931
+ return { did: did.join(" "), flagged };
9932
+ }
9933
+ function and(parts) {
9934
+ return parts.length < 2 ? parts[0] ?? "" : `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
9935
+ }
9936
+ function whatAmdahlDid(result) {
9937
+ const steps = result.unchanged === true ? { did: "", flagged: [] } : readSteps(list(result.steps) ?? []);
9938
+ const flagged = [...steps.flagged];
9939
+ let checked = "";
9638
9940
  let removed = "";
9639
9941
  const evidence = record(result.evidence);
9640
9942
  const claims = list(result.unsupported_claims);
@@ -9673,6 +9975,13 @@ function whatAmdahlDid(result) {
9673
9975
  const kept = new Set([...lines].sort((a, b) => a.rank - b.rank).slice(0, MAX_LINES));
9674
9976
  return lines.filter((l) => kept.has(l)).map((l) => l.line);
9675
9977
  }
9978
+ function feedbackLines(ctx, result) {
9979
+ const runId = text(result.run_id);
9980
+ if (!runId) return [];
9981
+ if (!feedbackLinkOn(ctx.io, ctx.config)) return [`Run: ${runId}`];
9982
+ const url = `${consoleUrlFor(ctx.io, ctx.apiUrl)}/admin/optimizer/runs/${encodeURIComponent(runId)}`;
9983
+ return [`Run: ${runId}`, `Feedback: ${url}`];
9984
+ }
9676
9985
  var PASTE_PROMPT = "Paste an email or LinkedIn message, then press Enter on an empty line:";
9677
9986
  var VERSION_RULE = "\u2500\u2500 Amdahl's version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
9678
9987
  function nextTimeLine(ctx) {
@@ -9745,14 +10054,14 @@ ${nextTimeLine(ctx)}`);
9745
10054
  if (result.unchanged === true) out.info("No rewrite beat your draft; it is returned unchanged.");
9746
10055
  out.result({}, String(result.message ?? ""));
9747
10056
  const did = whatAmdahlDid(result);
9748
- out.info([...did, nextTimeLine(ctx)].join("\n"));
10057
+ out.info([...did, ...feedbackLines(ctx, result), nextTimeLine(ctx)].join("\n"));
9749
10058
  return EXIT.ok;
9750
10059
  }
9751
10060
  async function optimize(ctx, args) {
9752
10061
  const { io, out } = ctx;
9753
10062
  let files = args;
9754
10063
  if (files.length === 0) {
9755
- if (io.stdinIsTTY && !ctx.nonInteractive) return optimizePasted(ctx);
10064
+ if (io.stdinIsTTY && !ctx.nonInteractive && runningAgent(ctx) === null && !ciVariable(io.env)) return optimizePasted(ctx);
9756
10065
  if (io.stdinIsTTY) {
9757
10066
  throw usageError(
9758
10067
  "Give a file to optimize, or pipe the message on stdin.",
@@ -9767,7 +10076,7 @@ async function optimize(ctx, args) {
9767
10076
  for (const file of files) {
9768
10077
  let message2;
9769
10078
  try {
9770
- message2 = file === "-" ? await io.readStdin() : readFileSync4(file, "utf8");
10079
+ message2 = file === "-" ? await io.readStdin() : readFileSync5(file, "utf8");
9771
10080
  } catch (err) {
9772
10081
  throw usageError(`Could not read ${file}: ${err instanceof Error ? err.message : String(err)}`);
9773
10082
  }
@@ -9791,292 +10100,14 @@ async function optimize(ctx, args) {
9791
10100
  if (result.unchanged === true) out.info("No rewrite beat your draft; it is returned unchanged.");
9792
10101
  else if (typeof result.summary === "string") out.info(result.summary);
9793
10102
  out.result({}, String(result.message ?? ""));
9794
- const did = whatAmdahlDid(result);
9795
- if (did.length) out.info(did.join("\n"));
10103
+ const lines = [...whatAmdahlDid(result), ...feedbackLines(ctx, result)];
10104
+ if (lines.length) out.info(lines.join("\n"));
9796
10105
  }
9797
10106
  }
9798
10107
  if (out.json) out.result(results.length === 1 ? results[0] : results);
9799
10108
  return exit;
9800
10109
  }
9801
10110
 
9802
- // src/commands/skills-guide.ts
9803
- import { existsSync as existsSync4 } from "fs";
9804
- import { dirname as dirname3, join as join6, relative, sep } from "path";
9805
-
9806
- // src/commands/skills-install.ts
9807
- 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";
9809
- import { dirname as dirname2, join as join5 } from "path";
9810
- function skillDirs(targets, name) {
9811
- const dirs = [];
9812
- if (targets.claude) dirs.push(join5(targets.base, ".claude", "skills", name));
9813
- if (targets.agents) dirs.push(join5(targets.base, ".agents", "skills", name));
9814
- return dirs;
9815
- }
9816
- var message = (err) => err instanceof Error ? err.message : String(err);
9817
- function plainReason(err) {
9818
- const code = err.code;
9819
- if (code === "EACCES" || code === "EPERM") return "you do not have permission to write there";
9820
- if (code === "ENOSPC") return "the disk is full";
9821
- if (code === "EROFS") return "that disk is read-only";
9822
- if (code === "ENOTDIR" || code === "EEXIST") return "a file is in the way";
9823
- return message(err);
9824
- }
9825
- var leftBehind = (written) => written.length > 0 ? `It wrote ${written.join(" and ")} first.` : "Nothing was written.";
9826
- function writeFailure(path, err, written = []) {
9827
- const code = err.code;
9828
- const left = leftBehind(written);
9829
- const fix = code === "EACCES" || code === "EPERM" ? " Run the command from a folder you can write to." : "";
9830
- return new CliError("write_failed", `Could not write ${path}: ${plainReason(err)}`, EXIT.general, `${left}${fix}`);
9831
- }
9832
- function sameFolder(dir, archive) {
9833
- let top;
9834
- try {
9835
- top = lstatSync2(dir);
9836
- } catch (err) {
9837
- if (err.code === "ENOENT") return null;
9838
- throw writeFailure(dir, err);
9839
- }
9840
- if (!top.isDirectory()) return false;
9841
- const want = new Map(archive.files.map((f) => [f.path, f]));
9842
- const wantDirs = new Set(archive.dirs);
9843
- let found = 0;
9844
- const walk = (rel) => {
9845
- for (const entry of readdirSync(join5(dir, rel), { withFileTypes: true })) {
9846
- const path = rel ? `${rel}/${entry.name}` : entry.name;
9847
- if (++found > want.size + wantDirs.size) return false;
9848
- if (entry.isDirectory()) {
9849
- if (!wantDirs.has(path) || !walk(path)) return false;
9850
- } else if (entry.isFile()) {
9851
- const file = want.get(path);
9852
- if (!file) return false;
9853
- const full = join5(dir, path);
9854
- if (process.platform !== "win32" && (lstatSync2(full).mode & 73 ? 493 : 420) !== file.mode) return false;
9855
- if (!readFileSync5(full).equals(file.bytes)) return false;
9856
- } else {
9857
- return false;
9858
- }
9859
- }
9860
- return true;
9861
- };
9862
- try {
9863
- return walk("") && found === want.size + wantDirs.size;
9864
- } catch (err) {
9865
- throw writeFailure(dir, err);
9866
- }
9867
- }
9868
- function refuseLinks(dir, file, written = []) {
9869
- const path = file ?? dir;
9870
- for (const part of [dirname2(dirname2(dir)), dirname2(dir), dir, ...file ? [file] : []]) {
9871
- let stat;
9872
- try {
9873
- stat = lstatSync2(part);
9874
- } catch (err) {
9875
- if (err.code === "ENOENT") return;
9876
- throw writeFailure(path, err, written);
9877
- }
9878
- const why = stat.isSymbolicLink() ? "is a link" : part === file && !stat.isFile() ? "is not a file" : "";
9879
- if (why) {
9880
- throw new CliError(
9881
- "link_in_the_way",
9882
- `Could not write ${path}: ${part} ${why}.`,
9883
- EXIT.general,
9884
- `${leftBehind(written)} Remove it, or install somewhere else.`
9885
- );
9886
- }
9887
- }
9888
- }
9889
- function planInstall(dirs, payload) {
9890
- return dirs.map((dir) => {
9891
- if (payload.type === "archive") {
9892
- refuseLinks(dir);
9893
- const same = sameFolder(dir, payload.archive);
9894
- return { path: dir, action: same === null ? "wrote" : same ? "unchanged" : "replaced" };
9895
- }
9896
- const path = join5(dir, "SKILL.md");
9897
- refuseLinks(dir, path);
9898
- let current;
9899
- try {
9900
- current = lstatSync2(path, { throwIfNoEntry: false }) ? readFileSync5(path) : null;
9901
- } catch (err) {
9902
- throw writeFailure(path, err);
9903
- }
9904
- return { path, action: current === null ? "wrote" : current.equals(payload.body) ? "unchanged" : "replaced" };
9905
- });
9906
- }
9907
- function refuseClashes(plan, name, force) {
9908
- const clashes = plan.filter((p) => p.action === "replaced").map((p) => p.path);
9909
- if (clashes.length > 0 && !force) {
9910
- throw new CliError(
9911
- "skill_exists",
9912
- `A different ${name} skill is already at ${clashes.join(" and ")}.`,
9913
- EXIT.general,
9914
- "Nothing was written. Run again with --force to replace it."
9915
- );
9916
- }
9917
- }
9918
- function stage(target, name, archive, staged) {
9919
- const s = { target, temp: "" };
9920
- staged.push(s);
9921
- refuseLinks(target);
9922
- s.createdParent = mkdirSync3(dirname2(target), { recursive: true }) ?? void 0;
9923
- refuseLinks(target);
9924
- s.temp = mkdtempSync(join5(dirname2(target), `.${name}.amdahl-tmp-`));
9925
- chmodSync4(s.temp, 493);
9926
- for (const dir of archive.dirs) mkdirSync3(join5(s.temp, dir));
9927
- for (const file of archive.files) writeFileSync3(join5(s.temp, file.path), file.bytes, { flag: "wx", mode: file.mode });
9928
- }
9929
- function rollBack(staged) {
9930
- for (const s of [...staged].reverse()) {
9931
- try {
9932
- if (s.swapped) {
9933
- rmSync3(s.target, { recursive: true, force: true });
9934
- if (s.backup) renameSync3(s.backup, s.target);
9935
- } else if (s.backup) {
9936
- renameSync3(s.backup, s.target);
9937
- }
9938
- } catch {
9939
- }
9940
- if (s.temp) rmSync3(s.temp, { recursive: true, force: true });
9941
- if (s.createdParent) rmSync3(s.createdParent, { recursive: true, force: true });
9942
- }
9943
- }
9944
- function writeFolders(plan, name, archive) {
9945
- const staged = [];
9946
- let current = "";
9947
- try {
9948
- for (const p of plan) {
9949
- if (p.action === "unchanged") continue;
9950
- current = p.path;
9951
- stage(p.path, name, archive, staged);
9952
- }
9953
- for (const s of staged) {
9954
- current = s.target;
9955
- if (plan.find((p) => p.path === s.target)?.action === "replaced") {
9956
- s.backup = `${s.temp}.old`;
9957
- renameSync3(s.target, s.backup);
9958
- }
9959
- renameSync3(s.temp, s.target);
9960
- s.swapped = true;
9961
- }
9962
- } catch (err) {
9963
- rollBack(staged);
9964
- if (err instanceof CliError) throw err;
9965
- throw writeFailure(current, err);
9966
- }
9967
- for (const s of staged) if (s.backup) rmSync3(s.backup, { recursive: true, force: true });
9968
- }
9969
- function writeFiles(plan, body) {
9970
- const written = [];
9971
- for (const p of plan) {
9972
- if (p.action === "unchanged") continue;
9973
- refuseLinks(dirname2(p.path), p.path, written);
9974
- const temp = join5(dirname2(p.path), `.SKILL.md.amdahl-tmp-${randomBytes3(6).toString("hex")}`);
9975
- try {
9976
- mkdirSync3(dirname2(p.path), { recursive: true });
9977
- refuseLinks(dirname2(p.path), p.path, written);
9978
- writeFileSync3(temp, body, { flag: "wx", mode: 420 });
9979
- renameSync3(temp, p.path);
9980
- } catch (err) {
9981
- rmSync3(temp, { force: true });
9982
- if (err instanceof CliError) throw err;
9983
- throw writeFailure(p.path, err, written);
9984
- }
9985
- written.push(p.path);
9986
- }
9987
- }
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;
10007
- }
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 };
10022
- }
10023
- function runningAgent(ctx) {
10024
- if (ctx.io.env.CLAUDECODE) return "claude";
10025
- if (ctx.io.env.CURSOR_AGENT) return "cursor";
10026
- return null;
10027
- }
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]}`;
10031
- }
10032
- function readable(slug) {
10033
- const words = slug.replace(/-/g, " ");
10034
- return words.charAt(0).toUpperCase() + words.slice(1);
10035
- }
10036
- async function guide(ctx, skill, payload) {
10037
- try {
10038
- return await ask(ctx, skill, payload);
10039
- } catch (err) {
10040
- if (err instanceof PromptCancelled) throw new CliError("cancelled", "Stopped. Nothing was written.", EXIT.cancelled);
10041
- throw err;
10042
- }
10043
- }
10044
- async function ask(ctx, skill, payload) {
10045
- 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;
10056
- }
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))));
10073
- }
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.`;
10078
- }
10079
-
10080
10111
  // src/commands/setup.ts
10081
10112
  var SHOWN = [
10082
10113
  { client: "claude-code", name: "Claude Code" },
@@ -10179,12 +10210,6 @@ function finishLines(names, inClaudeCode = false) {
10179
10210
  };
10180
10211
  return [`Try it: ${AGENT_ASK} in any of them.`, "Before that, connect each one once:", ...names.map((n) => ` ${n}: ${step[n] ?? ""}`)];
10181
10212
  }
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
10213
  function setupMode(io) {
10189
10214
  const { env } = io;
10190
10215
  const ci = ciVariable(env);
@@ -10256,7 +10281,7 @@ async function codexScreen(ctx, tool, say, detail, lead) {
10256
10281
  say(tool.already ? "\u2713 Codex already has Amdahl." : "\u2713 Added Amdahl to Codex.");
10257
10282
  detail(tool.detail);
10258
10283
  for (const line of lead) say(line);
10259
- if (!await onPath(ctx, "codex")) {
10284
+ if (ctx.io.platform === "win32" || !await onPath(ctx, "codex")) {
10260
10285
  say("One step left, in your terminal:");
10261
10286
  say(` 1. Run: ${CODEX_LOGIN}`);
10262
10287
  say(` 2. ${TRY_IN.Codex ?? ""}`);
@@ -10310,13 +10335,15 @@ ${installLine()}
10310
10335
  if (detected.length > 0) {
10311
10336
  const there = await Promise.all(detected.map((c) => hasAmdahl(ctx, c.client)));
10312
10337
  if (there.every(Boolean)) {
10338
+ let exit2 = EXIT.ok;
10313
10339
  for (const tool of detected) {
10314
10340
  if (await isInstalled(ctx, tool.client)) continue;
10315
- const { ready } = await installEach(ctx, [tool]);
10316
- for (const r of ready) detail(r.detail);
10341
+ const refreshed = await installEach(ctx, [tool]);
10342
+ for (const r of refreshed.ready) detail(r.detail);
10343
+ if (exit2 === EXIT.ok) exit2 = refreshed.exit;
10317
10344
  }
10318
10345
  for (const line of allSetLines(detected.map((c) => c.name))) say(line);
10319
- return EXIT.ok;
10346
+ return exit2;
10320
10347
  }
10321
10348
  }
10322
10349
  const rows = [...SHOWN.map((c) => c.name), TERMINAL];
@@ -11002,9 +11029,6 @@ async function workspaceUse(ctx, args) {
11002
11029
  return EXIT.ok;
11003
11030
  }
11004
11031
 
11005
- // src/version.ts
11006
- var VERSION = "0.3.5";
11007
-
11008
11032
  // src/main.ts
11009
11033
  var COMMANDS = {
11010
11034
  login: (ctx) => login(ctx),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amdahl/cli",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
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",