@amdahl/cli 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +35 -1
  2. package/dist/amdahl.js +109 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -105,7 +105,7 @@ amdahl keys list
105
105
  amdahl keys revoke <id|prefix> [--yes] [--no-browser]
106
106
  amdahl workspace list | amdahl workspace use <profile>
107
107
  amdahl install claude-code|codex|cursor [--print] [--force]
108
- amdahl skills add <publisher>/<skill> [-g] [--agents] [--force] [--site <origin>]
108
+ amdahl skills add <publisher>/<skill> [-g] [--agents] [--force] [--accept-warning] [--site <origin>]
109
109
  ```
110
110
 
111
111
  Global flags: `--profile`, `--api-url`, `--api-key`, `--json`, `--no-color`,
@@ -164,6 +164,39 @@ Code. Without flags it says on stderr where the skill went. `-y` (`--yes`)
164
164
  adds it to this project without asking. Neither replaces a different copy
165
165
  without `--force`.
166
166
 
167
+ amdahl.ai's AI security review flags some skills. A flagged skill's index
168
+ entry carries an `x-amdahl-warning` field. `skills add` prints it before it
169
+ writes anything: the skill, the sentence from the skill page, and the page's
170
+ link.
171
+
172
+ ```
173
+ Warning: amdahl.ai's security review flagged amdahl/call-prep.
174
+ Flagged: it may send your data somewhere else (line 14), and 2 more. Read it before you install.
175
+ See what was flagged: https://amdahl.ai/skills/amdahl/call-prep
176
+ ```
177
+
178
+ In a terminal, it then asks `Install this skill?` in the same kind of menu.
179
+ The marker starts on "Cancel", which writes nothing and exits 0. "Install
180
+ anyway" goes on as usual. With `-y`, `--json` or no terminal, it does not
181
+ install the skill. It does not ask an agent either, even in a terminal: when
182
+ `CLAUDECODE` or `CURSOR_AGENT` is set, it does not install. It says to show
183
+ the warning to the person, and exits 9. Once they agree, run the same
184
+ command again with `--accept-warning`: it prints the warning and installs. With `--json`, the result or the refusal
185
+ carries the warning as `warning`, with `result`, `flagged`, `line`, `text`
186
+ and `page`.
187
+
188
+ It treats every field of the warning as untrusted. It turns line breaks in
189
+ the sentence into spaces, removes control, escape and format characters, and
190
+ cuts the sentence at 300 characters. It keeps `line` only as a positive
191
+ integer, and `flagged` only as a plain id.
192
+
193
+ The site sends the field only for a flagged skill. So when the field is
194
+ there but `skills add` cannot read it (it is not an object, its `result` is
195
+ not `warn` or `fail`, or it has no sentence), the skill is still flagged. It
196
+ prints the sentence "amdahl.ai flagged this skill. This version of the CLI
197
+ could not read why." and holds the install the same way. Only a missing
198
+ field, or `null`, means no warning.
199
+
167
200
  It reads the skill's index,
168
201
  `https://amdahl.ai/skills/<publisher>/<skill>/.well-known/agent-skills/index.json`
169
202
  ([Agent Skills Discovery](https://schemas.agentskills.io/discovery/0.2.0/schema.json)
@@ -274,6 +307,7 @@ code shown in the terminal; then the key is printed once and stored nowhere.
274
307
  | 6 | rate limited or quota (429) |
275
308
  | 7 | approval denied, expired or locked out |
276
309
  | 8 | network or 5xx |
310
+ | 9 | `skills add` did not install a skill amdahl.ai's security review flagged: no terminal to ask, `-y`, `--json` or an agent ran it, and no `--accept-warning` (nothing was written) |
277
311
  | 130 | stopped with Ctrl+C at a question (nothing was written) |
278
312
 
279
313
  ## Development
package/dist/amdahl.js CHANGED
@@ -7547,6 +7547,8 @@ var EXIT = {
7547
7547
  rateLimited: 6,
7548
7548
  denied: 7,
7549
7549
  network: 8,
7550
+ /** `skills add` would install a skill amdahl.ai's security review flagged, with no terminal to ask and no --accept-warning. */
7551
+ flagged: 9,
7550
7552
  cancelled: 130
7551
7553
  };
7552
7554
  var CliError = class extends Error {
@@ -7555,13 +7557,16 @@ var CliError = class extends Error {
7555
7557
  hint;
7556
7558
  /** Technical detail for `--json` only, kept out of the plain message. */
7557
7559
  detail;
7558
- constructor(code, message2, exit, hint, detail) {
7560
+ /** More fields for `--json` only, printed beside `ok` and `error`. */
7561
+ data;
7562
+ constructor(code, message2, exit, hint, detail, data) {
7559
7563
  super(message2);
7560
7564
  this.name = "CliError";
7561
7565
  this.code = code;
7562
7566
  this.exit = exit;
7563
7567
  this.hint = hint;
7564
7568
  this.detail = detail;
7569
+ this.data = data;
7565
7570
  }
7566
7571
  };
7567
7572
  function usageError(message2, hint) {
@@ -7637,6 +7642,7 @@ var COMMAND_OPTIONS = {
7637
7642
  global: { type: "boolean", short: "g" },
7638
7643
  agents: { type: "boolean" },
7639
7644
  force: { type: "boolean" },
7645
+ "accept-warning": { type: "boolean" },
7640
7646
  site: { type: "string" }
7641
7647
  }
7642
7648
  };
@@ -7720,12 +7726,17 @@ Commands:
7720
7726
  keys revoke <id|prefix> [--yes] [--no-browser]
7721
7727
  workspace list | workspace use <profile>
7722
7728
  install claude-code|codex|cursor [--print] [--force]
7723
- skills add <publisher>/<skill> [-y] [-g] [--agents] [--force] [--site <origin>]
7729
+ skills add <publisher>/<skill> [-y] [-g] [--agents] [--force] [--accept-warning] [--site <origin>]
7724
7730
  In a terminal it asks one question: this project (Enter) or every project.
7725
7731
  -y add without asking, to this project
7726
7732
  -g every project (~/.claude/skills), not just this one
7727
7733
  --agents also .agents/skills, for Codex, Cursor and other agents
7728
7734
  --force replace a different copy of the skill already there
7735
+ --accept-warning
7736
+ install a skill that amdahl.ai's security review flagged,
7737
+ after printing the warning. Without it, a terminal asks
7738
+ (Cancel or Install anyway); -y, --json or no terminal
7739
+ stops (exit 9)
7729
7740
 
7730
7741
  Global options:
7731
7742
  --profile <name> Use a saved profile (default: AMDAHL_PROFILE, then the default profile)
@@ -8667,7 +8678,7 @@ async function installMcp(ctx, client, print) {
8667
8678
  const mcpUrl = `${ctx.apiUrl}/mcp`;
8668
8679
  const { io } = ctx;
8669
8680
  if (client === "claude-code") {
8670
- const args = ["mcp", "add", "--transport", "http", "amdahl", mcpUrl];
8681
+ const args = ["mcp", "add", "--scope", "user", "--transport", "http", "amdahl", mcpUrl];
8671
8682
  const command = `claude ${args.join(" ")}`;
8672
8683
  if (print || !await onPath(ctx, "claude")) {
8673
8684
  const notes = print ? [] : ["`claude` is not on your PATH. Run the command above once it is installed."];
@@ -8677,7 +8688,7 @@ async function installMcp(ctx, client, print) {
8677
8688
  if (res.code !== 0) {
8678
8689
  throw new CliError("install_failed", `\`${command}\` failed: ${res.stderr.trim() || `exit ${res.code}`}`, EXIT.general);
8679
8690
  }
8680
- return { data: { ok: true, client, action: "ran", command }, human: `Added Amdahl to Claude Code. Run /mcp in Claude Code to sign in.`, notes: [] };
8691
+ 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: [] };
8681
8692
  }
8682
8693
  if (client === "codex") {
8683
8694
  const path2 = join4(io.homedir, ".codex", "config.toml");
@@ -9491,6 +9502,80 @@ function nextStep(ctx, targets, seen) {
9491
9502
  return `Open a new ${joinNames(names, "or")} chat ${where} to use it.`;
9492
9503
  }
9493
9504
 
9505
+ // src/commands/skills-warning.ts
9506
+ var WARNING_FIELD = "x-amdahl-warning";
9507
+ var MAX_WARNING_CHARS = 300;
9508
+ var BREAK_RE = /[\t\n\v\f\r\u0085\u2028\u2029]/g;
9509
+ var UNSAFE_RE = /[\p{Cc}\p{Cf}\p{Cs}]/gu;
9510
+ var CHECK_ID_RE = /^[a-z][a-z0-9_]{0,39}$/;
9511
+ function warningText(raw) {
9512
+ const clean = raw.replace(BREAK_RE, " ").replace(UNSAFE_RE, "").replace(/ {2,}/g, " ").trim();
9513
+ const chars = Array.from(clean);
9514
+ if (chars.length <= MAX_WARNING_CHARS) return clean;
9515
+ return `${chars.slice(0, MAX_WARNING_CHARS - 3).join("").trimEnd()}...`;
9516
+ }
9517
+ var UNREADABLE_WARNING = Object.freeze({
9518
+ result: "warn",
9519
+ flagged: null,
9520
+ line: null,
9521
+ text: "amdahl.ai flagged this skill. This version of the CLI could not read why."
9522
+ });
9523
+ function readWarning(raw) {
9524
+ if (raw === void 0 || raw === null) return void 0;
9525
+ if (typeof raw !== "object" || Array.isArray(raw)) return { ...UNREADABLE_WARNING };
9526
+ const { result, flagged, line, text } = raw;
9527
+ if (result !== "warn" && result !== "fail") return { ...UNREADABLE_WARNING };
9528
+ const sentence = typeof text === "string" ? warningText(text) : "";
9529
+ if (!sentence) return { ...UNREADABLE_WARNING };
9530
+ return {
9531
+ result,
9532
+ flagged: typeof flagged === "string" && CHECK_ID_RE.test(flagged) ? flagged : null,
9533
+ line: typeof line === "number" && Number.isSafeInteger(line) && line > 0 ? line : null,
9534
+ text: sentence
9535
+ };
9536
+ }
9537
+ function skillPage(site, ref) {
9538
+ return `${site}/skills/${ref.publisher}/${ref.skill}`;
9539
+ }
9540
+ function warningJson(warning, site, ref) {
9541
+ return { ...warning, page: skillPage(site, ref) };
9542
+ }
9543
+ function canAsk(ctx) {
9544
+ return !ctx.nonInteractive && ctx.io.stdoutIsTTY && !ctx.out.json && ctx.flags.yes !== true && runningAgent(ctx) === null;
9545
+ }
9546
+ async function confirmWarning(ctx, ref, site, warning) {
9547
+ const { out } = ctx;
9548
+ const host = new URL(site).host;
9549
+ const name = `${ref.publisher}/${ref.skill}`;
9550
+ const head = warning.result === "fail" ? `${name} failed ${host}'s security review.` : `${host}'s security review flagged ${name}.`;
9551
+ out.info([out.bold(`Warning: ${head}`), ` ${warning.text}`, ` See what was flagged: ${skillPage(site, ref)}`].join("\n"));
9552
+ if (ctx.flags["accept-warning"] === true) {
9553
+ out.info("Installing it anyway, because of --accept-warning.");
9554
+ return true;
9555
+ }
9556
+ if (!canAsk(ctx)) {
9557
+ throw new CliError(
9558
+ "skill_flagged",
9559
+ `Did not install ${name}, because the security review flagged it. Nothing was written.`,
9560
+ EXIT.flagged,
9561
+ "Show this warning to the person you are working for. Once they agree, run the same command again with --accept-warning.",
9562
+ void 0,
9563
+ { warning: warningJson(warning, site, ref) }
9564
+ );
9565
+ }
9566
+ out.info("");
9567
+ let pick;
9568
+ try {
9569
+ pick = await ctx.io.select("Install this skill?", ["Cancel", "Install anyway"], { initial: 0, color: out.color });
9570
+ } catch (err) {
9571
+ if (err instanceof PromptCancelled) throw new CliError("cancelled", "Stopped. Nothing was written.", EXIT.cancelled);
9572
+ throw err;
9573
+ }
9574
+ if (pick === 1) return true;
9575
+ out.info("Cancelled. Nothing was written.");
9576
+ return false;
9577
+ }
9578
+
9494
9579
  // src/commands/skills.ts
9495
9580
  var DEFAULT_SKILLS_SITE = "https://amdahl.ai";
9496
9581
  var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
@@ -9501,7 +9586,7 @@ var MAX_SKILL_BYTES = 64 * 1024;
9501
9586
  var MAX_ARCHIVE_BYTES = 4 * 1024 * 1024;
9502
9587
  var DIGEST_RE = /^sha256:([0-9a-f]{64})$/i;
9503
9588
  var FRONT_MATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
9504
- var USAGE2 = "Usage: amdahl skills add <publisher>/<skill> [-y] [-g] [--agents] [--force] [--site <origin>]";
9589
+ var USAGE2 = "Usage: amdahl skills add <publisher>/<skill> [-y] [-g] [--agents] [--force] [--accept-warning] [--site <origin>]";
9505
9590
  function normaliseSite(raw) {
9506
9591
  let url;
9507
9592
  try {
@@ -9618,12 +9703,14 @@ function readIndex(body, indexUrl, ref, site) {
9618
9703
  if (!url) throw bad(`gives ${ref.skill} no file URL`);
9619
9704
  if (url.origin !== site) throw bad(`points ${ref.skill} at ${url.origin}, not ${site}`);
9620
9705
  const version = typeof entry.version === "string" ? printable(entry.version) : "";
9706
+ const warning = readWarning(entry[WARNING_FIELD]);
9621
9707
  return {
9622
9708
  type,
9623
9709
  url: url.href,
9624
9710
  sha256: digest[1].toLowerCase(),
9625
9711
  description: typeof entry.description === "string" ? entry.description : "",
9626
- ...version ? { version } : {}
9712
+ ...version ? { version } : {},
9713
+ ...warning ? { warning } : {}
9627
9714
  };
9628
9715
  }
9629
9716
  function checkDigest(body, fileUrl, sha256) {
@@ -9720,6 +9807,11 @@ async function skillsAdd(ctx, args) {
9720
9807
  if (guided) ctx.io.stderr("OK\n\n");
9721
9808
  const { entry, payload, meta } = fetched;
9722
9809
  const { url, sha256 } = entry;
9810
+ const { warning } = entry;
9811
+ if (warning) {
9812
+ if (!await confirmWarning(ctx, ref, site, warning)) return EXIT.ok;
9813
+ if (guided) ctx.out.info("");
9814
+ }
9723
9815
  const agent = runningAgent(ctx);
9724
9816
  let targets;
9725
9817
  let seen = [];
@@ -9749,7 +9841,15 @@ async function skillsAdd(ctx, args) {
9749
9841
  (f) => f.action === "unchanged" ? `Already up to date: ${f.path}` : `${f.action === "wrote" ? "Wrote" : "Replaced"} ${f.path}${count}`
9750
9842
  );
9751
9843
  if (contents && plan.some((f) => f.action !== "unchanged")) lines.push(...contents.map((p) => ` ${p}`));
9752
- const result = { ok: true, publisher: ref.publisher, skill: ref.skill, url, digest: `sha256:${sha256}`, files: plan };
9844
+ const result = {
9845
+ ok: true,
9846
+ publisher: ref.publisher,
9847
+ skill: ref.skill,
9848
+ url,
9849
+ digest: `sha256:${sha256}`,
9850
+ files: plan,
9851
+ ...warning ? { warning: warningJson(warning, site, ref) } : {}
9852
+ };
9753
9853
  if (guided) {
9754
9854
  const changed = plan.filter((f) => f.action !== "unchanged");
9755
9855
  const done = changed.length > 0 ? `Done. Added to ${skillsFolders(ctx, changed)}.` : `Already up to date in ${skillsFolders(ctx, plan)}.`;
@@ -9828,7 +9928,7 @@ async function workspaceUse(ctx, args) {
9828
9928
  }
9829
9929
 
9830
9930
  // src/version.ts
9831
- var VERSION = "0.2.1";
9931
+ var VERSION = "0.2.3";
9832
9932
 
9833
9933
  // src/main.ts
9834
9934
  var COMMANDS = {
@@ -9879,7 +9979,7 @@ async function runCli(argv, io) {
9879
9979
  const error = { code: e.code, message: e.message };
9880
9980
  if (e.hint) error.hint = e.hint;
9881
9981
  if (e.detail) error.detail = e.detail;
9882
- io.stdout(`${JSON.stringify({ ok: false, error })}
9982
+ io.stdout(`${JSON.stringify({ ...e.data, ok: false, error })}
9883
9983
  `);
9884
9984
  } else {
9885
9985
  io.stderr(`${e.code === "cancelled" ? "" : "Error: "}${e.message}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amdahl/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",