@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.
- package/README.md +34 -4
- package/dist/amdahl.js +418 -394
- 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,
|
|
144
|
-
|
|
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.
|
|
324
|
-
|
|
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
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
var
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
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
|
-
|
|
9542
|
-
|
|
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
|
-
|
|
9587
|
+
top = lstatSync2(dir);
|
|
9545
9588
|
} catch (err) {
|
|
9546
|
-
|
|
9589
|
+
if (err.code === "ENOENT") return null;
|
|
9590
|
+
throw writeFailure(dir, err);
|
|
9547
9591
|
}
|
|
9548
|
-
if (!
|
|
9549
|
-
|
|
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
|
|
9593
|
-
|
|
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
|
|
9596
|
-
return
|
|
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
|
|
9599
|
-
|
|
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
|
|
9602
|
-
|
|
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
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
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
|
|
9632
|
-
|
|
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
|
|
9635
|
-
const
|
|
9636
|
-
const
|
|
9637
|
-
|
|
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() :
|
|
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
|
|
9795
|
-
if (
|
|
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
|
|
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
|
|
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