@amdahl/cli 0.2.4 → 0.3.0

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 +42 -2
  2. package/dist/amdahl.js +569 -367
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,10 +56,48 @@ amdahl login
56
56
  `npx @amdahl/cli ...`. That package ([packages/amdahl](packages/amdahl)) has no
57
57
  code of its own: it depends on `@amdahl/cli` at the same version and runs it.
58
58
 
59
- To connect an AI client, run `amdahl install claude-code` (or `codex`,
59
+ The quickest way to start is `npx amdahl` with nothing after it, in a
60
+ terminal. It walks you through setup:
61
+
62
+ ```
63
+ Amdahl, predictive ML for GTM
64
+ Found on this machine: Claude Code ✓ Cursor ✓ Codex –
65
+ Set up Amdahl in:
66
+ > [x] Claude Code
67
+ [x] Cursor
68
+ [ ] Codex
69
+ (Use the arrow keys, Space to tick or untick, then Enter)
70
+ ```
71
+
72
+ It finds Claude Code, Cursor and Codex by their command on your PATH
73
+ (`claude`, `cursor`, `codex`) or their folder in your home folder (`.claude`,
74
+ `.cursor`, `.codex`), and ticks the ones it finds. Enter accepts. If you are
75
+ not signed in, it signs you in through the browser first; otherwise it prints
76
+ `Signed in as <email> (workspace: <name>)`. If the server rejects a key set in
77
+ `AMDAHL_KEY` (or `AMDAHL_API_KEY`, `AMDAHL_ACCESS_TOKEN`, `--api-key`), it says
78
+ so and skips the browser sign-in, since that key would still win. Then it
79
+ runs `amdahl install` for each client you ticked. A client that fails prints
80
+ its error and the rest still run; the exit code is the first failure's. It
81
+ ends with something to try, such as
82
+ `Try it: open Claude Code and say "make this email sound less like AI"`
83
+ (for Codex, it first says to run `codex mcp login amdahl`).
84
+ Run it again once you are set up and it says `You're set up in Claude Code
85
+ and Cursor.` with the same line to try, and asks nothing; if it cannot reach
86
+ Amdahl to check your sign-in, it still says so. Ctrl+C at the question stops
87
+ it with nothing written (exit 130).
88
+
89
+ It only does this for a person at a terminal. When stdin, stdout or stderr
90
+ (where the menu draws) is not a terminal, `CI` is set, an agent runs it (`CLAUDECODE` or `CURSOR_AGENT` is
91
+ set), `AMDAHL_NO_PROMPT=1` is set, or any flag is given (such as `--json`),
92
+ bare `amdahl` prints the usage to stderr and exits 2, as before.
93
+
94
+ To connect one AI client yourself, run `amdahl install claude-code` (or `codex`,
60
95
  `cursor`). It adds the Amdahl MCP server and also installs the Amdahl skill,
61
96
  a map of everything Amdahl can do, so your agent knows which tool fits each
62
- job. The skill goes to the client's user skills folder:
97
+ job. If the client already points at Amdahl, `install` leaves that as it is
98
+ (for Claude Code it skips `claude mcp add` when `claude mcp get amdahl`
99
+ shows the same URL) and still installs the skill. If it cannot write the
100
+ client's config file, it exits 1 with `write_failed` and names the file. The skill goes to the client's user skills folder:
63
101
 
64
102
  | client | skill file |
65
103
  |---|---|
@@ -77,6 +115,7 @@ https://docs.amdahl.ai/skills/amdahl/SKILL.md.
77
115
  ## Quick start
78
116
 
79
117
  ```sh
118
+ amdahl # in a terminal: guided setup for Claude Code, Cursor and Codex
80
119
  amdahl login # sign in through the browser with your console account
81
120
  amdahl whoami # the account, workspace and role you are signed in as
82
121
  amdahl status # can this workspace optimize, and what blocks it
@@ -93,6 +132,7 @@ On a server or in CI, skip `login` and set an API key in `AMDAHL_KEY`.
93
132
  ## Commands
94
133
 
95
134
  ```
135
+ amdahl (guided setup, in a terminal)
96
136
  amdahl login [--workspace <slug>] [--no-browser] [--port <n>] (alias: auth login)
97
137
  amdahl logout [--all] (alias: auth logout)
98
138
  amdahl whoami [--show-token] (alias: auth status)
package/dist/amdahl.js CHANGED
@@ -7371,26 +7371,25 @@ import { createInterface } from "readline";
7371
7371
  // src/select.ts
7372
7372
  import { emitKeypressEvents } from "readline";
7373
7373
  var HINT = "(Use the arrow keys, then Enter)";
7374
+ var MULTI_HINT = "(Use the arrow keys, Space to tick or untick, then Enter)";
7375
+ var OFF = "\x1B[0m";
7374
7376
  var HIDE_CURSOR = "\x1B[?25l";
7375
7377
  var SHOW_CURSOR = "\x1B[?25h";
7376
7378
  var CLEAR_LINE = "\x1B[2K";
7377
7379
  var CLEAR_DOWN = "\x1B[0J";
7378
7380
  var up = (n) => `\x1B[${n}A`;
7379
- function selectMenu(input, output, question, choices, options = {}) {
7380
- let current = Math.min(Math.max(options.initial ?? 0, 0), choices.length - 1);
7381
- const on = options.color ? options.trueColor ? "\x1B[1;38;2;253;75;3m" : "\x1B[1;38;5;202m" : "\x1B[1m";
7382
- const off = "\x1B[0m";
7383
- const row = (i) => i === current ? `${on}> ${choices[i]}${off}` : ` ${choices[i]}`;
7384
- const draw = () => choices.map((_, i) => `${CLEAR_LINE}\r${row(i)}
7385
- `).join("") + `${CLEAR_LINE}\r${HINT}
7381
+ function runMenu(input, output, question, hint, rows, onKey) {
7382
+ const draw = () => rows().map((r) => `${CLEAR_LINE}\r${r}
7383
+ `).join("") + `${CLEAR_LINE}\r${hint}
7386
7384
  `;
7385
+ const height = rows().length;
7387
7386
  const wasRaw = input.isRaw === true;
7388
7387
  return new Promise((resolve, reject) => {
7389
7388
  let done = false;
7390
7389
  const finish = (outcome) => {
7391
7390
  if (done) return;
7392
7391
  done = true;
7393
- input.removeListener("keypress", onKey);
7392
+ input.removeListener("keypress", onKeypress);
7394
7393
  input.removeListener("error", onError);
7395
7394
  process.removeListener("SIGINT", onSigint);
7396
7395
  try {
@@ -7399,7 +7398,7 @@ function selectMenu(input, output, question, choices, options = {}) {
7399
7398
  }
7400
7399
  input.pause();
7401
7400
  if ("pick" in outcome) {
7402
- output.write(`${up(choices.length + 2)}\r${CLEAR_DOWN}${question} ${choices[outcome.pick]}
7401
+ output.write(`${up(height + 2)}\r${CLEAR_DOWN}${question} ${outcome.summary}
7403
7402
  ${SHOW_CURSOR}`);
7404
7403
  resolve(outcome.pick);
7405
7404
  } else {
@@ -7407,17 +7406,12 @@ ${SHOW_CURSOR}`);
7407
7406
  reject(outcome.error);
7408
7407
  }
7409
7408
  };
7410
- const onKey = (text, key) => {
7409
+ const onKeypress = (text, key) => {
7411
7410
  const name = key?.name ?? text;
7412
7411
  if (key?.ctrl && (name === "c" || name === "d")) return finish({ error: new PromptCancelled() });
7413
- if (name === "return" || name === "enter") return finish({ pick: current });
7414
- let next = current;
7415
- if (name === "up" || name === "k") next = (current - 1 + choices.length) % choices.length;
7416
- else if (name === "down" || name === "j") next = (current + 1) % choices.length;
7417
- else if (text && /^[1-9]$/.test(text) && Number(text) <= choices.length) next = Number(text) - 1;
7418
- if (next === current) return;
7419
- current = next;
7420
- output.write(`${up(choices.length + 1)}${draw()}`);
7412
+ const result = onKey(text, name);
7413
+ if (result === "redraw") output.write(`${up(height + 1)}${draw()}`);
7414
+ else if (result) finish(result);
7421
7415
  };
7422
7416
  const onError = (err) => finish({ error: err });
7423
7417
  const onSigint = () => finish({ error: new PromptCancelled() });
@@ -7426,7 +7420,7 @@ ${SHOW_CURSOR}`);
7426
7420
  input.setRawMode?.(true);
7427
7421
  output.write(`${HIDE_CURSOR}${question}
7428
7422
  ${draw()}`);
7429
- input.on("keypress", onKey);
7423
+ input.on("keypress", onKeypress);
7430
7424
  input.on("error", onError);
7431
7425
  process.on("SIGINT", onSigint);
7432
7426
  input.resume();
@@ -7435,6 +7429,55 @@ ${draw()}`);
7435
7429
  }
7436
7430
  });
7437
7431
  }
7432
+ function marker(options) {
7433
+ return options.color ? options.trueColor ? "\x1B[1;38;2;253;75;3m" : "\x1B[1;38;5;202m" : "\x1B[1m";
7434
+ }
7435
+ function move(current, count, text, name) {
7436
+ if (name === "up" || name === "k") return (current - 1 + count) % count;
7437
+ if (name === "down" || name === "j") return (current + 1) % count;
7438
+ if (text && /^[1-9]$/.test(text) && Number(text) <= count) return Number(text) - 1;
7439
+ return null;
7440
+ }
7441
+ function selectMenu(input, output, question, choices, options = {}) {
7442
+ let current = Math.min(Math.max(options.initial ?? 0, 0), choices.length - 1);
7443
+ const on = marker(options);
7444
+ const rows = () => choices.map((c, i) => i === current ? `${on}> ${c}${OFF}` : ` ${c}`);
7445
+ return runMenu(input, output, question, HINT, rows, (text, name) => {
7446
+ if (name === "return" || name === "enter") return { pick: current, summary: choices[current] ?? "" };
7447
+ const next = move(current, choices.length, text, name);
7448
+ if (next === null || next === current) return null;
7449
+ current = next;
7450
+ return "redraw";
7451
+ });
7452
+ }
7453
+ function multiSelectMenu(input, output, question, choices, options = {}) {
7454
+ let current = 0;
7455
+ const ticked = choices.map((_, i) => options.ticked?.[i] === true);
7456
+ const on = marker(options);
7457
+ const rows = () => choices.map((c, i) => {
7458
+ const row = `[${ticked[i] ? "x" : " "}] ${c}`;
7459
+ return i === current ? `${on}> ${row}${OFF}` : ` ${row}`;
7460
+ });
7461
+ return runMenu(input, output, question, MULTI_HINT, rows, (text, name) => {
7462
+ if (name === "return" || name === "enter") {
7463
+ const picks = choices.flatMap((_, i) => ticked[i] ? [i] : []);
7464
+ return { pick: picks, summary: picks.map((i) => choices[i]).join(", ") || "none" };
7465
+ }
7466
+ if (name === "space") {
7467
+ ticked[current] = !ticked[current];
7468
+ return "redraw";
7469
+ }
7470
+ if (text && /^[1-9]$/.test(text) && Number(text) <= choices.length) {
7471
+ current = Number(text) - 1;
7472
+ ticked[current] = !ticked[current];
7473
+ return "redraw";
7474
+ }
7475
+ const next = move(current, choices.length, text, name);
7476
+ if (next === null || next === current) return null;
7477
+ current = next;
7478
+ return "redraw";
7479
+ });
7480
+ }
7438
7481
 
7439
7482
  // src/io.ts
7440
7483
  var PromptCancelled = class extends Error {
@@ -7516,6 +7559,10 @@ function realIo() {
7516
7559
  ...options,
7517
7560
  trueColor: /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? "")
7518
7561
  }),
7562
+ multiSelect: (question, choices, options) => multiSelectMenu(process.stdin, process.stderr, question, choices, {
7563
+ ...options,
7564
+ trueColor: /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? "")
7565
+ }),
7519
7566
  uninterruptible,
7520
7567
  run: runProcess,
7521
7568
  openUrl: async (url) => {
@@ -7704,6 +7751,8 @@ function strictParse(argv, options) {
7704
7751
  var USAGE = `Usage: amdahl <command> [options]
7705
7752
 
7706
7753
  What you can do:
7754
+ amdahl Set up Claude Code, Cursor or Codex, in a terminal: it asks
7755
+ which, signs you in, and runs install for each one
7707
7756
  amdahl optimize draft.md Rewrite an outbound email or LinkedIn message
7708
7757
  amdahl status Check that this workspace is set up to optimize
7709
7758
  amdahl keys create --name ci Make an API key for a server or CI
@@ -8623,6 +8672,12 @@ function skillPath(client, home) {
8623
8672
 
8624
8673
  // src/commands/install.ts
8625
8674
  var CLIENTS = ["claude-code", "codex", "cursor"];
8675
+ function codexConfigPath(home) {
8676
+ return join4(home, ".codex", "config.toml");
8677
+ }
8678
+ function cursorConfigPath(home) {
8679
+ return join4(home, ".cursor", "mcp.json");
8680
+ }
8626
8681
  function codexBlock(mcpUrl) {
8627
8682
  return `[mcp_servers.amdahl]
8628
8683
  url = "${mcpUrl}"
@@ -8637,6 +8692,50 @@ async function onPath(ctx, command) {
8637
8692
  const res = await ctx.io.run(probe, [command]);
8638
8693
  return res.code === 0;
8639
8694
  }
8695
+ var CLIENT_SIGNS = {
8696
+ "claude-code": { command: "claude", dir: ".claude" },
8697
+ codex: { command: "codex", dir: ".codex" },
8698
+ cursor: { command: "cursor", dir: ".cursor" }
8699
+ };
8700
+ async function detectClients(ctx) {
8701
+ const found = await Promise.all(
8702
+ CLIENTS.map(async (client) => {
8703
+ const { command, dir } = CLIENT_SIGNS[client];
8704
+ return existsSync3(join4(ctx.io.homedir, dir)) || await onPath(ctx, command);
8705
+ })
8706
+ );
8707
+ return Object.fromEntries(CLIENTS.map((client, i) => [client, found[i]]));
8708
+ }
8709
+ async function claudeHasAmdahl(ctx, mcpUrl) {
8710
+ const res = await ctx.io.run("claude", ["mcp", "get", "amdahl"]);
8711
+ return res.code === 0 && res.stdout.split("\n").some((line) => line.trim() === `URL: ${mcpUrl}`);
8712
+ }
8713
+ function updateConfig(path, fn) {
8714
+ try {
8715
+ return fn();
8716
+ } catch (err) {
8717
+ if (err instanceof CliError) throw err;
8718
+ const reason = err instanceof Error ? err.message : String(err);
8719
+ throw new CliError("write_failed", `Could not update ${path} (${reason}).`, EXIT.general);
8720
+ }
8721
+ }
8722
+ async function isInstalled(ctx, client) {
8723
+ const skill = skillPath(client, ctx.io.homedir);
8724
+ const current = existsSync3(skill) ? readFileSync3(skill, "utf8") : null;
8725
+ if (current === null || current !== SKILL_TEXT && isShippedSkill(current)) return false;
8726
+ const mcpUrl = `${ctx.apiUrl}/mcp`;
8727
+ if (client === "claude-code") return await onPath(ctx, "claude") && await claudeHasAmdahl(ctx, mcpUrl);
8728
+ if (client === "codex") {
8729
+ const path = codexConfigPath(ctx.io.homedir);
8730
+ return existsSync3(path) && codexHasAmdahl(readFileSync3(path, "utf8"));
8731
+ }
8732
+ try {
8733
+ const config = JSON.parse(readFileSync3(cursorConfigPath(ctx.io.homedir), "utf8"));
8734
+ return JSON.stringify(config.mcpServers?.amdahl) === JSON.stringify({ url: mcpUrl });
8735
+ } catch {
8736
+ return false;
8737
+ }
8738
+ }
8640
8739
  async function install(ctx, args) {
8641
8740
  const client = args[0];
8642
8741
  if (!client || args.length > 1 || !CLIENTS.includes(client)) {
@@ -8685,6 +8784,9 @@ async function installMcp(ctx, client, print) {
8685
8784
  const notes = print ? [] : ["`claude` is not on your PATH. Run the command above once it is installed."];
8686
8785
  return { data: { ok: true, client, action: "printed", command }, human: command, notes };
8687
8786
  }
8787
+ if (await claudeHasAmdahl(ctx, mcpUrl)) {
8788
+ return { data: { ok: true, client, action: "unchanged", command }, human: `Claude Code already has Amdahl at ${mcpUrl}; nothing changed.`, notes: [] };
8789
+ }
8688
8790
  const res = await io.run("claude", args);
8689
8791
  if (res.code !== 0) {
8690
8792
  throw new CliError("install_failed", `\`${command}\` failed: ${res.stderr.trim() || `exit ${res.code}`}`, EXIT.general);
@@ -8692,47 +8794,51 @@ async function installMcp(ctx, client, print) {
8692
8794
  return { data: { ok: true, client, action: "ran", command }, human: `Added Amdahl to Claude Code for all your projects. Run /mcp in Claude Code to sign in. If amdahl is not listed there, restart Claude Code first.`, notes: [] };
8693
8795
  }
8694
8796
  if (client === "codex") {
8695
- const path2 = join4(io.homedir, ".codex", "config.toml");
8797
+ const path2 = codexConfigPath(io.homedir);
8696
8798
  const block = codexBlock(mcpUrl);
8697
8799
  if (print) {
8698
8800
  return { data: { ok: true, client, action: "printed", path: path2 }, human: `# ${path2}
8699
8801
  ${block}`, notes: [] };
8700
8802
  }
8701
- const current = existsSync3(path2) ? readFileSync3(path2, "utf8") : "";
8702
- if (codexHasAmdahl(current)) {
8703
- return { data: { ok: true, client, action: "unchanged", path: path2 }, human: `${path2} already has [mcp_servers.amdahl]; nothing changed.`, notes: [] };
8704
- }
8705
- mkdirSync2(dirname(path2), { recursive: true });
8706
- const sep2 = current.length === 0 ? "" : current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
8707
- writeFileSync2(path2, `${current}${sep2}${block}`);
8708
- return { data: { ok: true, client, action: "wrote", path: path2 }, human: `Added Amdahl to ${path2}. Run \`codex mcp login amdahl\` to sign in.`, notes: [] };
8803
+ return updateConfig(path2, () => {
8804
+ const current = existsSync3(path2) ? readFileSync3(path2, "utf8") : "";
8805
+ if (codexHasAmdahl(current)) {
8806
+ return { data: { ok: true, client, action: "unchanged", path: path2 }, human: `${path2} already has [mcp_servers.amdahl]; nothing changed.`, notes: [] };
8807
+ }
8808
+ mkdirSync2(dirname(path2), { recursive: true });
8809
+ const sep2 = current.length === 0 ? "" : current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
8810
+ writeFileSync2(path2, `${current}${sep2}${block}`);
8811
+ return { data: { ok: true, client, action: "wrote", path: path2 }, human: `Added Amdahl to ${path2}. Run \`codex mcp login amdahl\` to sign in.`, notes: [] };
8812
+ });
8709
8813
  }
8710
- const path = join4(io.homedir, ".cursor", "mcp.json");
8814
+ const path = cursorConfigPath(io.homedir);
8711
8815
  const entry = { url: mcpUrl };
8712
8816
  if (print) {
8713
8817
  return { data: { ok: true, client, action: "printed", path }, human: `# ${path}
8714
8818
  ${JSON.stringify({ mcpServers: { amdahl: entry } }, null, 2)}`, notes: [] };
8715
8819
  }
8716
- let config = {};
8717
- if (existsSync3(path)) {
8718
- const raw = readFileSync3(path, "utf8");
8719
- if (raw.trim()) {
8720
- try {
8721
- config = JSON.parse(raw);
8722
- } catch {
8723
- throw new CliError("invalid_config", `${path} is not valid JSON; not changing it.`, EXIT.general);
8820
+ return updateConfig(path, () => {
8821
+ let config = {};
8822
+ if (existsSync3(path)) {
8823
+ const raw = readFileSync3(path, "utf8");
8824
+ if (raw.trim()) {
8825
+ try {
8826
+ config = JSON.parse(raw);
8827
+ } catch {
8828
+ throw new CliError("invalid_config", `${path} is not valid JSON; not changing it.`, EXIT.general);
8829
+ }
8724
8830
  }
8725
8831
  }
8726
- }
8727
- const servers = config.mcpServers && typeof config.mcpServers === "object" ? config.mcpServers : {};
8728
- if (JSON.stringify(servers.amdahl) === JSON.stringify(entry)) {
8729
- return { data: { ok: true, client, action: "unchanged", path }, human: `${path} already points at ${mcpUrl}; nothing changed.`, notes: [] };
8730
- }
8731
- config.mcpServers = { ...servers, amdahl: entry };
8732
- mkdirSync2(dirname(path), { recursive: true });
8733
- writeFileSync2(path, `${JSON.stringify(config, null, 2)}
8832
+ const servers = config.mcpServers && typeof config.mcpServers === "object" ? config.mcpServers : {};
8833
+ if (JSON.stringify(servers.amdahl) === JSON.stringify(entry)) {
8834
+ return { data: { ok: true, client, action: "unchanged", path }, human: `${path} already points at ${mcpUrl}; nothing changed.`, notes: [] };
8835
+ }
8836
+ config.mcpServers = { ...servers, amdahl: entry };
8837
+ mkdirSync2(dirname(path), { recursive: true });
8838
+ writeFileSync2(path, `${JSON.stringify(config, null, 2)}
8734
8839
  `);
8735
- return { data: { ok: true, client, action: "wrote", path }, human: `Added Amdahl to ${path}. Cursor asks you to sign in on first use.`, notes: [] };
8840
+ return { data: { ok: true, client, action: "wrote", path }, human: `Added Amdahl to ${path}. Cursor asks you to sign in on first use.`, notes: [] };
8841
+ });
8736
8842
  }
8737
8843
 
8738
8844
  // src/keygen.ts
@@ -9008,337 +9114,120 @@ async function optimize(ctx, args) {
9008
9114
  return exit;
9009
9115
  }
9010
9116
 
9011
- // src/commands/skills.ts
9012
- var import_yaml = __toESM(require_dist(), 1);
9013
- import { createHash as createHash4 } from "crypto";
9117
+ // src/commands/skills-guide.ts
9118
+ import { existsSync as existsSync4 } from "fs";
9119
+ import { dirname as dirname3, join as join6, relative, sep } from "path";
9014
9120
 
9015
- // src/output.ts
9016
- var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
9017
- function printable(text) {
9018
- return text.replace(CONTROL_RE, "");
9121
+ // src/commands/skills-install.ts
9122
+ import { randomBytes as randomBytes3 } from "crypto";
9123
+ import { chmodSync as chmodSync2, lstatSync, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
9124
+ import { dirname as dirname2, join as join5 } from "path";
9125
+ function skillDirs(targets, name) {
9126
+ const dirs = [];
9127
+ if (targets.claude) dirs.push(join5(targets.base, ".claude", "skills", name));
9128
+ if (targets.agents) dirs.push(join5(targets.base, ".agents", "skills", name));
9129
+ return dirs;
9019
9130
  }
9020
- function createOutput(io, json, noColor) {
9021
- const color = !noColor && !io.env.NO_COLOR && io.env.TERM !== "dumb" && io.stderrIsTTY && !json;
9022
- return {
9023
- json,
9024
- color,
9025
- result(data, human) {
9026
- if (json) {
9027
- io.stdout(`${JSON.stringify(data)}
9028
- `);
9029
- } else if (human !== void 0) {
9030
- io.stdout(human.endsWith("\n") ? human : `${human}
9031
- `);
9131
+ var message = (err) => err instanceof Error ? err.message : String(err);
9132
+ function plainReason(err) {
9133
+ const code = err.code;
9134
+ if (code === "EACCES" || code === "EPERM") return "you do not have permission to write there";
9135
+ if (code === "ENOSPC") return "the disk is full";
9136
+ if (code === "EROFS") return "that disk is read-only";
9137
+ if (code === "ENOTDIR" || code === "EEXIST") return "a file is in the way";
9138
+ return message(err);
9139
+ }
9140
+ var leftBehind = (written) => written.length > 0 ? `It wrote ${written.join(" and ")} first.` : "Nothing was written.";
9141
+ function writeFailure(path, err, written = []) {
9142
+ const code = err.code;
9143
+ const left = leftBehind(written);
9144
+ const fix = code === "EACCES" || code === "EPERM" ? " Run the command from a folder you can write to." : "";
9145
+ return new CliError("write_failed", `Could not write ${path}: ${plainReason(err)}`, EXIT.general, `${left}${fix}`);
9146
+ }
9147
+ function sameFolder(dir, archive) {
9148
+ let top;
9149
+ try {
9150
+ top = lstatSync(dir);
9151
+ } catch (err) {
9152
+ if (err.code === "ENOENT") return null;
9153
+ throw writeFailure(dir, err);
9154
+ }
9155
+ if (!top.isDirectory()) return false;
9156
+ const want = new Map(archive.files.map((f) => [f.path, f]));
9157
+ const wantDirs = new Set(archive.dirs);
9158
+ let found = 0;
9159
+ const walk = (rel) => {
9160
+ for (const entry of readdirSync(join5(dir, rel), { withFileTypes: true })) {
9161
+ const path = rel ? `${rel}/${entry.name}` : entry.name;
9162
+ if (++found > want.size + wantDirs.size) return false;
9163
+ if (entry.isDirectory()) {
9164
+ if (!wantDirs.has(path) || !walk(path)) return false;
9165
+ } else if (entry.isFile()) {
9166
+ const file = want.get(path);
9167
+ if (!file) return false;
9168
+ const full = join5(dir, path);
9169
+ if (process.platform !== "win32" && (lstatSync(full).mode & 73 ? 493 : 420) !== file.mode) return false;
9170
+ if (!readFileSync5(full).equals(file.bytes)) return false;
9171
+ } else {
9172
+ return false;
9032
9173
  }
9033
- },
9034
- info(text) {
9035
- io.stderr(text.endsWith("\n") ? text : `${text}
9036
- `);
9037
- },
9038
- event(data) {
9039
- if (json) io.stderr(`${JSON.stringify(data)}
9040
- `);
9041
- },
9042
- bold(text) {
9043
- return color ? `\x1B[1m${text}\x1B[22m` : text;
9044
9174
  }
9175
+ return true;
9045
9176
  };
9046
- }
9047
-
9048
- // src/tar-gz.ts
9049
- import { inflateRawSync } from "zlib";
9050
- var MAX_ARCHIVE_FILES = 200;
9051
- var MAX_UNPACKED_BYTES = 4 * 1024 * 1024;
9052
- var MAX_TAR_BYTES = MAX_UNPACKED_BYTES + 1024 * 1024;
9053
- var BLOCK = 512;
9054
- var FILE_MODES = /* @__PURE__ */ new Set([420, 493]);
9055
- var CONTROL_RE2 = /[\u0000-\u001f\u007f-\u009f]/;
9056
- var ArchiveError = class extends Error {
9057
- tooLarge;
9058
- constructor(message2, tooLarge = false) {
9059
- super(message2);
9060
- this.name = "ArchiveError";
9061
- this.tooLarge = tooLarge;
9177
+ try {
9178
+ return walk("") && found === want.size + wantDirs.size;
9179
+ } catch (err) {
9180
+ throw writeFailure(dir, err);
9062
9181
  }
9063
- };
9064
- var crcTable = null;
9065
- function crc32(data) {
9066
- if (!crcTable) {
9067
- crcTable = new Uint32Array(256);
9068
- for (let n = 0; n < 256; n += 1) {
9069
- let c = n;
9070
- for (let k = 0; k < 8; k += 1) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
9071
- crcTable[n] = c >>> 0;
9182
+ }
9183
+ function refuseLinks(dir, file, written = []) {
9184
+ const path = file ?? dir;
9185
+ for (const part of [dirname2(dirname2(dir)), dirname2(dir), dir, ...file ? [file] : []]) {
9186
+ let stat;
9187
+ try {
9188
+ stat = lstatSync(part);
9189
+ } catch (err) {
9190
+ if (err.code === "ENOENT") return;
9191
+ throw writeFailure(path, err, written);
9192
+ }
9193
+ const why = stat.isSymbolicLink() ? "is a link" : part === file && !stat.isFile() ? "is not a file" : "";
9194
+ if (why) {
9195
+ throw new CliError(
9196
+ "link_in_the_way",
9197
+ `Could not write ${path}: ${part} ${why}.`,
9198
+ EXIT.general,
9199
+ `${leftBehind(written)} Remove it, or install somewhere else.`
9200
+ );
9072
9201
  }
9073
9202
  }
9074
- let crc = 4294967295;
9075
- for (let i = 0; i < data.length; i += 1) crc = crcTable[(crc ^ data[i]) & 255] ^ crc >>> 8;
9076
- return (crc ^ 4294967295) >>> 0;
9077
9203
  }
9078
- function gunzipChecked(gz) {
9079
- if (gz.length < 18 || gz[0] !== 31 || gz[1] !== 139) throw new ArchiveError("it is not gzip");
9080
- if (gz[2] !== 8) throw new ArchiveError("its gzip method is not deflate");
9081
- const flags = gz[3];
9082
- if (flags & 224) throw new ArchiveError("its gzip header sets reserved flags");
9083
- let at = 10;
9084
- if (flags & 4) at += 2 + gz.readUInt16LE(Math.min(at, gz.length - 2));
9085
- for (const flag of [8, 16]) {
9086
- if (!(flags & flag)) continue;
9087
- const end = gz.indexOf(0, at);
9088
- if (end < 0) throw new ArchiveError("its gzip header is cut short");
9089
- at = end + 1;
9090
- }
9091
- if (flags & 2) at += 2;
9092
- if (at > gz.length - 8) throw new ArchiveError("its gzip header is cut short");
9093
- let tar;
9094
- let used;
9095
- try {
9096
- const out = inflateRawSync(gz.subarray(at), { info: true, maxOutputLength: MAX_TAR_BYTES });
9097
- tar = out.buffer;
9098
- used = out.engine.bytesWritten;
9099
- } catch (err) {
9100
- if (err.code === "ERR_BUFFER_TOO_LARGE") {
9101
- throw new ArchiveError(`it unpacks to more than ${MAX_TAR_BYTES / 1024 / 1024} MB`, true);
9204
+ function planInstall(dirs, payload) {
9205
+ return dirs.map((dir) => {
9206
+ if (payload.type === "archive") {
9207
+ refuseLinks(dir);
9208
+ const same = sameFolder(dir, payload.archive);
9209
+ return { path: dir, action: same === null ? "wrote" : same ? "unchanged" : "replaced" };
9102
9210
  }
9103
- throw new ArchiveError(`its gzip data is broken (${err instanceof Error ? err.message : String(err)})`);
9104
- }
9105
- const trailer = at + used;
9106
- if (gz.length - trailer < 8) throw new ArchiveError("its gzip trailer is cut short");
9107
- if (gz.length - trailer > 8) throw new ArchiveError("it has bytes after its gzip trailer");
9108
- if (gz.readUInt32LE(trailer) !== crc32(tar)) throw new ArchiveError("its gzip CRC-32 does not match its contents");
9109
- if (gz.readUInt32LE(trailer + 4) !== tar.length % 2 ** 32) throw new ArchiveError("its gzip length does not match its contents");
9110
- return tar;
9211
+ const path = join5(dir, "SKILL.md");
9212
+ refuseLinks(dir, path);
9213
+ let current;
9214
+ try {
9215
+ current = lstatSync(path, { throwIfNoEntry: false }) ? readFileSync5(path) : null;
9216
+ } catch (err) {
9217
+ throw writeFailure(path, err);
9218
+ }
9219
+ return { path, action: current === null ? "wrote" : current.equals(payload.body) ? "unchanged" : "replaced" };
9220
+ });
9111
9221
  }
9112
- function field(header, start, length) {
9113
- const raw = header.subarray(start, start + length);
9114
- const end = raw.indexOf(0);
9115
- try {
9116
- return new TextDecoder("utf-8", { fatal: true }).decode(end < 0 ? raw : raw.subarray(0, end));
9117
- } catch {
9118
- throw new ArchiveError("it has a path that is not UTF-8");
9119
- }
9120
- }
9121
- function octal(header, start, length) {
9122
- const text = header.toString("latin1", start, start + length).replace(/[\0 ]+$/, "").replace(/^ +/, "");
9123
- return /^[0-7]{1,12}$/.test(text) ? parseInt(text, 8) : null;
9124
- }
9125
- function checkPath(path) {
9126
- const shown = JSON.stringify(path);
9127
- if (path === "") throw new ArchiveError("it has an entry with an empty path");
9128
- if (path.startsWith("/")) throw new ArchiveError(`it has an absolute path, ${shown}`);
9129
- if (path.includes("\\")) throw new ArchiveError(`it has a path with a backslash, ${shown}`);
9130
- if (CONTROL_RE2.test(path)) throw new ArchiveError(`it has a path with a control character, ${shown}`);
9131
- for (const segment of path.split("/")) {
9132
- if (segment === "") throw new ArchiveError(`it has a path with an empty segment, ${shown}`);
9133
- if (segment === ".." || segment === ".") throw new ArchiveError(`it has a path with "${segment}" in it, ${shown}`);
9134
- }
9135
- }
9136
- var TYPE_NAMES = {
9137
- "1": "a hard link",
9138
- "2": "a symbolic link",
9139
- "3": "a character device",
9140
- "4": "a block device",
9141
- "6": "a fifo",
9142
- "7": "a contiguous file",
9143
- x: "a pax header",
9144
- g: "a pax global header",
9145
- L: "a GNU long name",
9146
- K: "a GNU long link name"
9147
- };
9148
- function readTar(tar) {
9149
- if (tar.length % BLOCK !== 0) throw new ArchiveError("its tar is not a whole number of 512-byte blocks");
9150
- const files = [];
9151
- const dirs = [];
9152
- const seen = /* @__PURE__ */ new Map();
9153
- let unpacked = 0;
9154
- let at = 0;
9155
- for (; ; ) {
9156
- if (at >= tar.length) throw new ArchiveError("its tar has no end marker");
9157
- const header = tar.subarray(at, at + BLOCK);
9158
- if (header.every((b) => b === 0)) {
9159
- if (!tar.subarray(at).every((b) => b === 0)) throw new ArchiveError("it has data after its tar end marker");
9160
- break;
9161
- }
9162
- let sum = 0;
9163
- for (let i = 0; i < BLOCK; i += 1) sum += i >= 148 && i < 156 ? 32 : header[i];
9164
- if (octal(header, 148, 8) !== sum) throw new ArchiveError("it has a tar header with a wrong checksum");
9165
- if (header.toString("latin1", 257, 263) !== "ustar\0" || header.toString("latin1", 263, 265) !== "00") {
9166
- throw new ArchiveError("its tar is not ustar");
9167
- }
9168
- const name = field(header, 0, 100);
9169
- const prefix = field(header, 345, 155);
9170
- let path = prefix ? `${prefix}/${name}` : name;
9171
- const type = String.fromCharCode(header[156]);
9172
- const size = octal(header, 124, 12);
9173
- const mode = octal(header, 100, 8);
9174
- if (size === null || mode === null) throw new ArchiveError(`it has a tar header for ${JSON.stringify(path)} with a bad size or mode`);
9175
- const isDir = type === "5";
9176
- if (!isDir && type !== "0" && type !== "\0") {
9177
- const what = TYPE_NAMES[type] ?? `an entry of type ${JSON.stringify(type)}`;
9178
- throw new ArchiveError(`${JSON.stringify(path)} is ${what}; a skill archive holds only files and folders`);
9179
- }
9180
- if (isDir && path.endsWith("/")) path = path.slice(0, -1);
9181
- checkPath(path);
9182
- const key = path.toLowerCase();
9183
- const parts = key.split("/");
9184
- for (let i = 1; i < parts.length; i += 1) {
9185
- const parent = parts.slice(0, i).join("/");
9186
- const kind = seen.get(parent);
9187
- if (kind === "file") throw new ArchiveError(`it puts ${JSON.stringify(path)} inside a file`);
9188
- if (!kind) seen.set(parent, "implied");
9189
- }
9190
- const before = seen.get(key);
9191
- if (before === "file" || before === "dir" || before === "implied" && !isDir) {
9192
- throw new ArchiveError(`it lists ${JSON.stringify(path)} twice`);
9193
- }
9194
- const start = at + BLOCK;
9195
- if (isDir) {
9196
- if (size !== 0) throw new ArchiveError(`its folder ${JSON.stringify(path)} has a size`);
9197
- seen.set(key, "dir");
9198
- dirs.push(path);
9199
- } else {
9200
- if (!FILE_MODES.has(mode & 4095)) {
9201
- throw new ArchiveError(`${JSON.stringify(path)} has mode ${(mode & 4095).toString(8).padStart(4, "0")}; a file must be 0644 or 0755`);
9202
- }
9203
- if (files.length + 1 > MAX_ARCHIVE_FILES) throw new ArchiveError(`it has more than ${MAX_ARCHIVE_FILES} files`, true);
9204
- unpacked += size;
9205
- if (unpacked > MAX_UNPACKED_BYTES) throw new ArchiveError(`its files come to more than ${MAX_UNPACKED_BYTES / 1024 / 1024} MB`, true);
9206
- if (start + size > tar.length) throw new ArchiveError(`its tar is cut short inside ${JSON.stringify(path)}`);
9207
- seen.set(key, "file");
9208
- files.push({ path, bytes: Buffer.from(tar.subarray(start, start + size)), mode: mode & 4095 });
9209
- }
9210
- at = start + Math.ceil(size / BLOCK) * BLOCK;
9211
- }
9212
- const allDirs = /* @__PURE__ */ new Map();
9213
- const addDirsOf = (path, self) => {
9214
- const parts = path.split("/");
9215
- for (let i = 1; i <= parts.length - (self ? 0 : 1); i += 1) {
9216
- const dir = parts.slice(0, i).join("/");
9217
- if (!allDirs.has(dir.toLowerCase())) allDirs.set(dir.toLowerCase(), dir);
9218
- }
9219
- };
9220
- for (const dir of dirs) addDirsOf(dir, true);
9221
- for (const f of files) addDirsOf(f.path, false);
9222
- return { files, dirs: [...allDirs.values()].sort((a, b) => a.split("/").length - b.split("/").length) };
9223
- }
9224
- function readSkillArchive(gz) {
9225
- return readTar(gunzipChecked(gz));
9226
- }
9227
-
9228
- // src/commands/skills-guide.ts
9229
- import { existsSync as existsSync4 } from "fs";
9230
- import { dirname as dirname3, join as join6, relative, sep } from "path";
9231
-
9232
- // src/commands/skills-install.ts
9233
- import { randomBytes as randomBytes3 } from "crypto";
9234
- import { chmodSync as chmodSync2, lstatSync, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
9235
- import { dirname as dirname2, join as join5 } from "path";
9236
- function skillDirs(targets, name) {
9237
- const dirs = [];
9238
- if (targets.claude) dirs.push(join5(targets.base, ".claude", "skills", name));
9239
- if (targets.agents) dirs.push(join5(targets.base, ".agents", "skills", name));
9240
- return dirs;
9241
- }
9242
- var message = (err) => err instanceof Error ? err.message : String(err);
9243
- function plainReason(err) {
9244
- const code = err.code;
9245
- if (code === "EACCES" || code === "EPERM") return "you do not have permission to write there";
9246
- if (code === "ENOSPC") return "the disk is full";
9247
- if (code === "EROFS") return "that disk is read-only";
9248
- if (code === "ENOTDIR" || code === "EEXIST") return "a file is in the way";
9249
- return message(err);
9250
- }
9251
- var leftBehind = (written) => written.length > 0 ? `It wrote ${written.join(" and ")} first.` : "Nothing was written.";
9252
- function writeFailure(path, err, written = []) {
9253
- const code = err.code;
9254
- const left = leftBehind(written);
9255
- const fix = code === "EACCES" || code === "EPERM" ? " Run the command from a folder you can write to." : "";
9256
- return new CliError("write_failed", `Could not write ${path}: ${plainReason(err)}`, EXIT.general, `${left}${fix}`);
9257
- }
9258
- function sameFolder(dir, archive) {
9259
- let top;
9260
- try {
9261
- top = lstatSync(dir);
9262
- } catch (err) {
9263
- if (err.code === "ENOENT") return null;
9264
- throw writeFailure(dir, err);
9265
- }
9266
- if (!top.isDirectory()) return false;
9267
- const want = new Map(archive.files.map((f) => [f.path, f]));
9268
- const wantDirs = new Set(archive.dirs);
9269
- let found = 0;
9270
- const walk = (rel) => {
9271
- for (const entry of readdirSync(join5(dir, rel), { withFileTypes: true })) {
9272
- const path = rel ? `${rel}/${entry.name}` : entry.name;
9273
- if (++found > want.size + wantDirs.size) return false;
9274
- if (entry.isDirectory()) {
9275
- if (!wantDirs.has(path) || !walk(path)) return false;
9276
- } else if (entry.isFile()) {
9277
- const file = want.get(path);
9278
- if (!file) return false;
9279
- const full = join5(dir, path);
9280
- if (process.platform !== "win32" && (lstatSync(full).mode & 73 ? 493 : 420) !== file.mode) return false;
9281
- if (!readFileSync5(full).equals(file.bytes)) return false;
9282
- } else {
9283
- return false;
9284
- }
9285
- }
9286
- return true;
9287
- };
9288
- try {
9289
- return walk("") && found === want.size + wantDirs.size;
9290
- } catch (err) {
9291
- throw writeFailure(dir, err);
9292
- }
9293
- }
9294
- function refuseLinks(dir, file, written = []) {
9295
- const path = file ?? dir;
9296
- for (const part of [dirname2(dirname2(dir)), dirname2(dir), dir, ...file ? [file] : []]) {
9297
- let stat;
9298
- try {
9299
- stat = lstatSync(part);
9300
- } catch (err) {
9301
- if (err.code === "ENOENT") return;
9302
- throw writeFailure(path, err, written);
9303
- }
9304
- const why = stat.isSymbolicLink() ? "is a link" : part === file && !stat.isFile() ? "is not a file" : "";
9305
- if (why) {
9306
- throw new CliError(
9307
- "link_in_the_way",
9308
- `Could not write ${path}: ${part} ${why}.`,
9309
- EXIT.general,
9310
- `${leftBehind(written)} Remove it, or install somewhere else.`
9311
- );
9312
- }
9313
- }
9314
- }
9315
- function planInstall(dirs, payload) {
9316
- return dirs.map((dir) => {
9317
- if (payload.type === "archive") {
9318
- refuseLinks(dir);
9319
- const same = sameFolder(dir, payload.archive);
9320
- return { path: dir, action: same === null ? "wrote" : same ? "unchanged" : "replaced" };
9321
- }
9322
- const path = join5(dir, "SKILL.md");
9323
- refuseLinks(dir, path);
9324
- let current;
9325
- try {
9326
- current = lstatSync(path, { throwIfNoEntry: false }) ? readFileSync5(path) : null;
9327
- } catch (err) {
9328
- throw writeFailure(path, err);
9329
- }
9330
- return { path, action: current === null ? "wrote" : current.equals(payload.body) ? "unchanged" : "replaced" };
9331
- });
9332
- }
9333
- function refuseClashes(plan, name, force) {
9334
- const clashes = plan.filter((p) => p.action === "replaced").map((p) => p.path);
9335
- if (clashes.length > 0 && !force) {
9336
- throw new CliError(
9337
- "skill_exists",
9338
- `A different ${name} skill is already at ${clashes.join(" and ")}.`,
9339
- EXIT.general,
9340
- "Nothing was written. Run again with --force to replace it."
9341
- );
9222
+ function refuseClashes(plan, name, force) {
9223
+ const clashes = plan.filter((p) => p.action === "replaced").map((p) => p.path);
9224
+ if (clashes.length > 0 && !force) {
9225
+ throw new CliError(
9226
+ "skill_exists",
9227
+ `A different ${name} skill is already at ${clashes.join(" and ")}.`,
9228
+ EXIT.general,
9229
+ "Nothing was written. Run again with --force to replace it."
9230
+ );
9342
9231
  }
9343
9232
  }
9344
9233
  function stage(target, name, archive, staged) {
@@ -9503,6 +9392,318 @@ function nextStep(ctx, targets, seen) {
9503
9392
  return `Open a new ${joinNames(names, "or")} chat ${where} to use it.`;
9504
9393
  }
9505
9394
 
9395
+ // src/commands/setup.ts
9396
+ var SHOWN = [
9397
+ { client: "claude-code", name: "Claude Code" },
9398
+ { client: "cursor", name: "Cursor" },
9399
+ { client: "codex", name: "Codex" }
9400
+ ];
9401
+ function tryItLine(name) {
9402
+ const ask2 = 'say "make this email sound less like AI"';
9403
+ return name === "Codex" ? `Try it: run \`codex mcp login amdahl\`, then open Codex and ${ask2}` : `Try it: open ${name} and ${ask2}`;
9404
+ }
9405
+ function shouldSetUp(io) {
9406
+ const { env } = io;
9407
+ const ci = Boolean(env.CI) && env.CI !== "false" && env.CI !== "0";
9408
+ return io.stdinIsTTY && io.stdoutIsTTY && io.stderrIsTTY && env.AMDAHL_NO_PROMPT !== "1" && !ci && !env.CLAUDECODE && !env.CURSOR_AGENT;
9409
+ }
9410
+ async function signIn(ctx) {
9411
+ let credential;
9412
+ try {
9413
+ credential = await resolveCredential(ctx);
9414
+ } catch (err) {
9415
+ if (err instanceof CliError && err.exit === EXIT.unauthenticated) return { kind: "out" };
9416
+ throw err;
9417
+ }
9418
+ try {
9419
+ const me = await call(() => apiClient(ctx, credential).cli.whoami());
9420
+ return { kind: "in", email: me.email ?? me.user_id, workspace: me.workspace?.name ?? me.business_id };
9421
+ } catch (err) {
9422
+ if (!(err instanceof CliError && err.exit === EXIT.unauthenticated)) throw err;
9423
+ return credential.source === "profile" ? { kind: "out" } : { kind: "rejected", source: credential.source };
9424
+ }
9425
+ }
9426
+ async function setup(ctx) {
9427
+ const { io, out } = ctx;
9428
+ const say = (line) => io.stdout(`${line}
9429
+ `);
9430
+ say(`${out.bold("Amdahl")}, predictive ML for GTM`);
9431
+ const found = await detectClients(ctx);
9432
+ const detected = SHOWN.filter((c) => found[c.client]);
9433
+ let who;
9434
+ if (detected.length > 0) {
9435
+ const installed = await Promise.all(detected.map((c) => isInstalled(ctx, c.client)));
9436
+ if (installed.every(Boolean)) {
9437
+ let unreachable = false;
9438
+ try {
9439
+ who = await signIn(ctx);
9440
+ } catch (err) {
9441
+ if (!(err instanceof CliError && err.exit === EXIT.network)) throw err;
9442
+ unreachable = true;
9443
+ }
9444
+ if (unreachable || who?.kind === "in") {
9445
+ say(`You're set up in ${joinNames(detected.map((c) => c.name))}.${unreachable ? " (Could not reach Amdahl to check your sign-in.)" : ""}`);
9446
+ say(tryItLine(detected[0]?.name ?? ""));
9447
+ return EXIT.ok;
9448
+ }
9449
+ }
9450
+ }
9451
+ say(`Found on this machine: ${SHOWN.map((c) => `${c.name} ${found[c.client] ? "\u2713" : "\u2013"}`).join(" ")}`);
9452
+ if (detected.length === 0) {
9453
+ say("None of them found here. Tick the ones you use with Space, then press Enter.");
9454
+ }
9455
+ const picks = await io.multiSelect(
9456
+ "Set up Amdahl in:",
9457
+ SHOWN.map((c) => c.name),
9458
+ { ticked: SHOWN.map((c) => found[c.client]), color: out.color }
9459
+ );
9460
+ if (picks.length === 0) {
9461
+ say("Nothing picked, so nothing changed. To set one up later: amdahl install claude-code|codex|cursor");
9462
+ return EXIT.ok;
9463
+ }
9464
+ who ??= await signIn(ctx);
9465
+ if (who.kind === "in") say(`Signed in as ${who.email} (workspace: ${who.workspace})`);
9466
+ else if (who.kind === "out") await login(ctx);
9467
+ else {
9468
+ const name = who.source === "flag" ? "--api-key" : who.source.slice("env:".length);
9469
+ out.info(`Amdahl rejected the key in ${name}. Skipping the browser sign-in, because that key would still be used instead. Fix it, or unset it and run \`amdahl login\`.`);
9470
+ }
9471
+ let exit = EXIT.ok;
9472
+ let ready = null;
9473
+ for (const { client, name } of picks.flatMap((i) => SHOWN[i] ?? [])) {
9474
+ try {
9475
+ await install(ctx, [client]);
9476
+ } catch (raw) {
9477
+ const err = raw instanceof CliError ? raw : new CliError("install_failed", `Could not set up ${name}: ${raw instanceof Error ? raw.message : String(raw)}`, EXIT.general);
9478
+ io.stderr(`Error: ${err.message}
9479
+ ${err.hint ? `${err.hint}
9480
+ ` : ""}`);
9481
+ if (exit === EXIT.ok) exit = err.exit;
9482
+ continue;
9483
+ }
9484
+ if (ready === null && (client !== "claude-code" || await onPath(ctx, "claude"))) ready = name;
9485
+ }
9486
+ if (ready) say(tryItLine(ready));
9487
+ return exit;
9488
+ }
9489
+
9490
+ // src/commands/skills.ts
9491
+ var import_yaml = __toESM(require_dist(), 1);
9492
+ import { createHash as createHash4 } from "crypto";
9493
+
9494
+ // src/output.ts
9495
+ var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
9496
+ function printable(text) {
9497
+ return text.replace(CONTROL_RE, "");
9498
+ }
9499
+ function createOutput(io, json, noColor) {
9500
+ const color = !noColor && !io.env.NO_COLOR && io.env.TERM !== "dumb" && io.stderrIsTTY && !json;
9501
+ return {
9502
+ json,
9503
+ color,
9504
+ result(data, human) {
9505
+ if (json) {
9506
+ io.stdout(`${JSON.stringify(data)}
9507
+ `);
9508
+ } else if (human !== void 0) {
9509
+ io.stdout(human.endsWith("\n") ? human : `${human}
9510
+ `);
9511
+ }
9512
+ },
9513
+ info(text) {
9514
+ io.stderr(text.endsWith("\n") ? text : `${text}
9515
+ `);
9516
+ },
9517
+ event(data) {
9518
+ if (json) io.stderr(`${JSON.stringify(data)}
9519
+ `);
9520
+ },
9521
+ bold(text) {
9522
+ return color ? `\x1B[1m${text}\x1B[22m` : text;
9523
+ }
9524
+ };
9525
+ }
9526
+
9527
+ // src/tar-gz.ts
9528
+ import { inflateRawSync } from "zlib";
9529
+ var MAX_ARCHIVE_FILES = 200;
9530
+ var MAX_UNPACKED_BYTES = 4 * 1024 * 1024;
9531
+ var MAX_TAR_BYTES = MAX_UNPACKED_BYTES + 1024 * 1024;
9532
+ var BLOCK = 512;
9533
+ var FILE_MODES = /* @__PURE__ */ new Set([420, 493]);
9534
+ var CONTROL_RE2 = /[\u0000-\u001f\u007f-\u009f]/;
9535
+ var ArchiveError = class extends Error {
9536
+ tooLarge;
9537
+ constructor(message2, tooLarge = false) {
9538
+ super(message2);
9539
+ this.name = "ArchiveError";
9540
+ this.tooLarge = tooLarge;
9541
+ }
9542
+ };
9543
+ var crcTable = null;
9544
+ function crc32(data) {
9545
+ if (!crcTable) {
9546
+ crcTable = new Uint32Array(256);
9547
+ for (let n = 0; n < 256; n += 1) {
9548
+ let c = n;
9549
+ for (let k = 0; k < 8; k += 1) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
9550
+ crcTable[n] = c >>> 0;
9551
+ }
9552
+ }
9553
+ let crc = 4294967295;
9554
+ for (let i = 0; i < data.length; i += 1) crc = crcTable[(crc ^ data[i]) & 255] ^ crc >>> 8;
9555
+ return (crc ^ 4294967295) >>> 0;
9556
+ }
9557
+ function gunzipChecked(gz) {
9558
+ if (gz.length < 18 || gz[0] !== 31 || gz[1] !== 139) throw new ArchiveError("it is not gzip");
9559
+ if (gz[2] !== 8) throw new ArchiveError("its gzip method is not deflate");
9560
+ const flags = gz[3];
9561
+ if (flags & 224) throw new ArchiveError("its gzip header sets reserved flags");
9562
+ let at = 10;
9563
+ if (flags & 4) at += 2 + gz.readUInt16LE(Math.min(at, gz.length - 2));
9564
+ for (const flag of [8, 16]) {
9565
+ if (!(flags & flag)) continue;
9566
+ const end = gz.indexOf(0, at);
9567
+ if (end < 0) throw new ArchiveError("its gzip header is cut short");
9568
+ at = end + 1;
9569
+ }
9570
+ if (flags & 2) at += 2;
9571
+ if (at > gz.length - 8) throw new ArchiveError("its gzip header is cut short");
9572
+ let tar;
9573
+ let used;
9574
+ try {
9575
+ const out = inflateRawSync(gz.subarray(at), { info: true, maxOutputLength: MAX_TAR_BYTES });
9576
+ tar = out.buffer;
9577
+ used = out.engine.bytesWritten;
9578
+ } catch (err) {
9579
+ if (err.code === "ERR_BUFFER_TOO_LARGE") {
9580
+ throw new ArchiveError(`it unpacks to more than ${MAX_TAR_BYTES / 1024 / 1024} MB`, true);
9581
+ }
9582
+ throw new ArchiveError(`its gzip data is broken (${err instanceof Error ? err.message : String(err)})`);
9583
+ }
9584
+ const trailer = at + used;
9585
+ if (gz.length - trailer < 8) throw new ArchiveError("its gzip trailer is cut short");
9586
+ if (gz.length - trailer > 8) throw new ArchiveError("it has bytes after its gzip trailer");
9587
+ if (gz.readUInt32LE(trailer) !== crc32(tar)) throw new ArchiveError("its gzip CRC-32 does not match its contents");
9588
+ if (gz.readUInt32LE(trailer + 4) !== tar.length % 2 ** 32) throw new ArchiveError("its gzip length does not match its contents");
9589
+ return tar;
9590
+ }
9591
+ function field(header, start, length) {
9592
+ const raw = header.subarray(start, start + length);
9593
+ const end = raw.indexOf(0);
9594
+ try {
9595
+ return new TextDecoder("utf-8", { fatal: true }).decode(end < 0 ? raw : raw.subarray(0, end));
9596
+ } catch {
9597
+ throw new ArchiveError("it has a path that is not UTF-8");
9598
+ }
9599
+ }
9600
+ function octal(header, start, length) {
9601
+ const text = header.toString("latin1", start, start + length).replace(/[\0 ]+$/, "").replace(/^ +/, "");
9602
+ return /^[0-7]{1,12}$/.test(text) ? parseInt(text, 8) : null;
9603
+ }
9604
+ function checkPath(path) {
9605
+ const shown = JSON.stringify(path);
9606
+ if (path === "") throw new ArchiveError("it has an entry with an empty path");
9607
+ if (path.startsWith("/")) throw new ArchiveError(`it has an absolute path, ${shown}`);
9608
+ if (path.includes("\\")) throw new ArchiveError(`it has a path with a backslash, ${shown}`);
9609
+ if (CONTROL_RE2.test(path)) throw new ArchiveError(`it has a path with a control character, ${shown}`);
9610
+ for (const segment of path.split("/")) {
9611
+ if (segment === "") throw new ArchiveError(`it has a path with an empty segment, ${shown}`);
9612
+ if (segment === ".." || segment === ".") throw new ArchiveError(`it has a path with "${segment}" in it, ${shown}`);
9613
+ }
9614
+ }
9615
+ var TYPE_NAMES = {
9616
+ "1": "a hard link",
9617
+ "2": "a symbolic link",
9618
+ "3": "a character device",
9619
+ "4": "a block device",
9620
+ "6": "a fifo",
9621
+ "7": "a contiguous file",
9622
+ x: "a pax header",
9623
+ g: "a pax global header",
9624
+ L: "a GNU long name",
9625
+ K: "a GNU long link name"
9626
+ };
9627
+ function readTar(tar) {
9628
+ if (tar.length % BLOCK !== 0) throw new ArchiveError("its tar is not a whole number of 512-byte blocks");
9629
+ const files = [];
9630
+ const dirs = [];
9631
+ const seen = /* @__PURE__ */ new Map();
9632
+ let unpacked = 0;
9633
+ let at = 0;
9634
+ for (; ; ) {
9635
+ if (at >= tar.length) throw new ArchiveError("its tar has no end marker");
9636
+ const header = tar.subarray(at, at + BLOCK);
9637
+ if (header.every((b) => b === 0)) {
9638
+ if (!tar.subarray(at).every((b) => b === 0)) throw new ArchiveError("it has data after its tar end marker");
9639
+ break;
9640
+ }
9641
+ let sum = 0;
9642
+ for (let i = 0; i < BLOCK; i += 1) sum += i >= 148 && i < 156 ? 32 : header[i];
9643
+ if (octal(header, 148, 8) !== sum) throw new ArchiveError("it has a tar header with a wrong checksum");
9644
+ if (header.toString("latin1", 257, 263) !== "ustar\0" || header.toString("latin1", 263, 265) !== "00") {
9645
+ throw new ArchiveError("its tar is not ustar");
9646
+ }
9647
+ const name = field(header, 0, 100);
9648
+ const prefix = field(header, 345, 155);
9649
+ let path = prefix ? `${prefix}/${name}` : name;
9650
+ const type = String.fromCharCode(header[156]);
9651
+ const size = octal(header, 124, 12);
9652
+ const mode = octal(header, 100, 8);
9653
+ if (size === null || mode === null) throw new ArchiveError(`it has a tar header for ${JSON.stringify(path)} with a bad size or mode`);
9654
+ const isDir = type === "5";
9655
+ if (!isDir && type !== "0" && type !== "\0") {
9656
+ const what = TYPE_NAMES[type] ?? `an entry of type ${JSON.stringify(type)}`;
9657
+ throw new ArchiveError(`${JSON.stringify(path)} is ${what}; a skill archive holds only files and folders`);
9658
+ }
9659
+ if (isDir && path.endsWith("/")) path = path.slice(0, -1);
9660
+ checkPath(path);
9661
+ const key = path.toLowerCase();
9662
+ const parts = key.split("/");
9663
+ for (let i = 1; i < parts.length; i += 1) {
9664
+ const parent = parts.slice(0, i).join("/");
9665
+ const kind = seen.get(parent);
9666
+ if (kind === "file") throw new ArchiveError(`it puts ${JSON.stringify(path)} inside a file`);
9667
+ if (!kind) seen.set(parent, "implied");
9668
+ }
9669
+ const before = seen.get(key);
9670
+ if (before === "file" || before === "dir" || before === "implied" && !isDir) {
9671
+ throw new ArchiveError(`it lists ${JSON.stringify(path)} twice`);
9672
+ }
9673
+ const start = at + BLOCK;
9674
+ if (isDir) {
9675
+ if (size !== 0) throw new ArchiveError(`its folder ${JSON.stringify(path)} has a size`);
9676
+ seen.set(key, "dir");
9677
+ dirs.push(path);
9678
+ } else {
9679
+ if (!FILE_MODES.has(mode & 4095)) {
9680
+ throw new ArchiveError(`${JSON.stringify(path)} has mode ${(mode & 4095).toString(8).padStart(4, "0")}; a file must be 0644 or 0755`);
9681
+ }
9682
+ if (files.length + 1 > MAX_ARCHIVE_FILES) throw new ArchiveError(`it has more than ${MAX_ARCHIVE_FILES} files`, true);
9683
+ unpacked += size;
9684
+ if (unpacked > MAX_UNPACKED_BYTES) throw new ArchiveError(`its files come to more than ${MAX_UNPACKED_BYTES / 1024 / 1024} MB`, true);
9685
+ if (start + size > tar.length) throw new ArchiveError(`its tar is cut short inside ${JSON.stringify(path)}`);
9686
+ seen.set(key, "file");
9687
+ files.push({ path, bytes: Buffer.from(tar.subarray(start, start + size)), mode: mode & 4095 });
9688
+ }
9689
+ at = start + Math.ceil(size / BLOCK) * BLOCK;
9690
+ }
9691
+ const allDirs = /* @__PURE__ */ new Map();
9692
+ const addDirsOf = (path, self) => {
9693
+ const parts = path.split("/");
9694
+ for (let i = 1; i <= parts.length - (self ? 0 : 1); i += 1) {
9695
+ const dir = parts.slice(0, i).join("/");
9696
+ if (!allDirs.has(dir.toLowerCase())) allDirs.set(dir.toLowerCase(), dir);
9697
+ }
9698
+ };
9699
+ for (const dir of dirs) addDirsOf(dir, true);
9700
+ for (const f of files) addDirsOf(f.path, false);
9701
+ return { files, dirs: [...allDirs.values()].sort((a, b) => a.split("/").length - b.split("/").length) };
9702
+ }
9703
+ function readSkillArchive(gz) {
9704
+ return readTar(gunzipChecked(gz));
9705
+ }
9706
+
9506
9707
  // src/commands/skills-warning.ts
9507
9708
  var WARNING_FIELD = "x-amdahl-warning";
9508
9709
  var MAX_WARNING_CHARS = 300;
@@ -9929,7 +10130,7 @@ async function workspaceUse(ctx, args) {
9929
10130
  }
9930
10131
 
9931
10132
  // src/version.ts
9932
- var VERSION = "0.2.4";
10133
+ var VERSION = "0.3.0";
9933
10134
 
9934
10135
  // src/main.ts
9935
10136
  var COMMANDS = {
@@ -9964,6 +10165,7 @@ async function runCli(argv, io) {
9964
10165
  return EXIT.ok;
9965
10166
  }
9966
10167
  if (!parsed.command) {
10168
+ if (argv.length === 0 && shouldSetUp(io)) return await setup(createCtx(io, out, parsed.flags));
9967
10169
  io.stderr(USAGE);
9968
10170
  return EXIT.usage;
9969
10171
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amdahl/cli",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
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",