@amdahl/cli 0.3.0 → 0.3.1
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 +35 -16
- package/dist/amdahl.js +217 -99
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,36 +60,48 @@ The quickest way to start is `npx amdahl` with nothing after it, in a
|
|
|
60
60
|
terminal. It walks you through setup:
|
|
61
61
|
|
|
62
62
|
```
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
Set up
|
|
63
|
+
$ npx amdahl
|
|
64
|
+
Amdahl · predictive ML for GTM
|
|
65
|
+
Set up in:
|
|
66
66
|
> [x] Claude Code
|
|
67
|
-
[
|
|
67
|
+
[ ] Cursor
|
|
68
68
|
[ ] Codex
|
|
69
69
|
(Use the arrow keys, Space to tick or untick, then Enter)
|
|
70
|
+
Opening your browser…
|
|
71
|
+
✓ Signed in · Acme
|
|
72
|
+
✓ Added to Claude Code
|
|
73
|
+
Try it: in Claude Code, run /mcp once to connect, then say "make this email sound less like AI"
|
|
70
74
|
```
|
|
71
75
|
|
|
72
76
|
It finds Claude Code, Cursor and Codex by their command on your PATH
|
|
73
77
|
(`claude`, `cursor`, `codex`) or their folder in your home folder (`.claude`,
|
|
74
78
|
`.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
|
|
76
|
-
|
|
79
|
+
not signed in, it signs you in through the browser first. It prints the
|
|
80
|
+
sign-in link only if the browser could not be opened, or nothing came back
|
|
81
|
+
within 5 seconds, and it waits up to 15 minutes, so someone new can create
|
|
82
|
+
their workspace on the way and land back in the same setup. If the server
|
|
83
|
+
rejects a key set in
|
|
77
84
|
`AMDAHL_KEY` (or `AMDAHL_API_KEY`, `AMDAHL_ACCESS_TOKEN`, `--api-key`), it says
|
|
78
85
|
so and skips the browser sign-in, since that key would still win. Then it
|
|
79
|
-
|
|
86
|
+
sets up each client you ticked the way `amdahl install` does, one line each.
|
|
87
|
+
A client that fails prints
|
|
80
88
|
its error and the rest still run; the exit code is the first failure's. It
|
|
81
|
-
ends with something to try
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
ends with something to try in the first client it set up. Each client then
|
|
90
|
+
signs in to Amdahl itself, once, the first time it connects: `/mcp` in
|
|
91
|
+
Claude Code, `codex mcp login amdahl` for Codex (always named, since the CLI
|
|
92
|
+
cannot tell whether it ran), and Cursor asks on first use.
|
|
93
|
+
`npx amdahl --verbose` also prints the files it wrote, the profile and where
|
|
94
|
+
the sign-in is stored.
|
|
84
95
|
Run it again once you are set up and it says `You're set up in Claude Code
|
|
85
|
-
and Cursor.` with
|
|
96
|
+
and Cursor.` with a line to try, and asks nothing; if it cannot reach
|
|
86
97
|
Amdahl to check your sign-in, it still says so. Ctrl+C at the question stops
|
|
87
98
|
it with nothing written (exit 130).
|
|
88
99
|
|
|
89
100
|
It only does this for a person at a terminal. When stdin, stdout or stderr
|
|
90
101
|
(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
|
|
92
|
-
bare `amdahl` prints the usage to stderr and exits 2, as
|
|
102
|
+
set), `AMDAHL_NO_PROMPT=1` is set, or any flag other than `--verbose` is given
|
|
103
|
+
(such as `--json`), bare `amdahl` prints the usage to stderr and exits 2, as
|
|
104
|
+
before.
|
|
93
105
|
|
|
94
106
|
To connect one AI client yourself, run `amdahl install claude-code` (or `codex`,
|
|
95
107
|
`cursor`). It adds the Amdahl MCP server and also installs the Amdahl skill,
|
|
@@ -124,8 +136,14 @@ amdahl install claude-code # connect an AI client and add the Amdahl skill (al
|
|
|
124
136
|
amdahl skills add <publisher>/<skill> # add a skill from amdahl.ai/skills to this project
|
|
125
137
|
```
|
|
126
138
|
|
|
127
|
-
No workspace yet?
|
|
128
|
-
|
|
139
|
+
No workspace yet? With a work email, the sign-in page offers to create your
|
|
140
|
+
company's workspace and then finishes signing you in. If you signed in with
|
|
141
|
+
an address Amdahl can't use, the browser tab and the terminal say what to do
|
|
142
|
+
and link a new sign-in ("Sign in with your work email", "Sign in again" for
|
|
143
|
+
a subdomain address, "Try again" once you've confirmed your email), and the
|
|
144
|
+
terminal keeps waiting for it. Anything
|
|
145
|
+
else exits 5 with the one next step (ask your company's admins, try again
|
|
146
|
+
tomorrow, and so on). The CLI itself never creates or joins a workspace.
|
|
129
147
|
|
|
130
148
|
On a server or in CI, skip `login` and set an API key in `AMDAHL_KEY`.
|
|
131
149
|
|
|
@@ -325,7 +343,8 @@ the default profile). `whoami` names the source that won. `keys create`,
|
|
|
325
343
|
tokens.
|
|
326
344
|
- Tokens: the OS keychain (service `amdahl-cli`; macOS `security`, Linux
|
|
327
345
|
`secret-tool`), passed on stdin, never on argv. Without a keychain:
|
|
328
|
-
`credentials.json` (mode 0600)
|
|
346
|
+
`credentials.json` (mode 0600), named under `--verbose`. If that file is
|
|
347
|
+
ever readable by other users, the CLI makes it owner-only and warns.
|
|
329
348
|
`AMDAHL_CREDENTIAL_STORE=keychain|file` forces either.
|
|
330
349
|
|
|
331
350
|
## Keys
|
package/dist/amdahl.js
CHANGED
|
@@ -7567,7 +7567,7 @@ function realIo() {
|
|
|
7567
7567
|
run: runProcess,
|
|
7568
7568
|
openUrl: async (url) => {
|
|
7569
7569
|
const [cmd, args] = openerFor(process.platform, url);
|
|
7570
|
-
await runProcess(cmd, args);
|
|
7570
|
+
return (await runProcess(cmd, args)).code === 0;
|
|
7571
7571
|
},
|
|
7572
7572
|
homedir: homedir(),
|
|
7573
7573
|
cwd: process.cwd(),
|
|
@@ -7575,7 +7575,8 @@ function realIo() {
|
|
|
7575
7575
|
hostname: hostname(),
|
|
7576
7576
|
now: () => Date.now(),
|
|
7577
7577
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
7578
|
-
loginTimeoutMs:
|
|
7578
|
+
loginTimeoutMs: 9e5,
|
|
7579
|
+
browserGraceMs: 5e3,
|
|
7579
7580
|
fetchTimeoutMs: 15e3
|
|
7580
7581
|
};
|
|
7581
7582
|
}
|
|
@@ -7654,7 +7655,8 @@ var GLOBAL_OPTIONS = {
|
|
|
7654
7655
|
json: { type: "boolean" },
|
|
7655
7656
|
"no-color": { type: "boolean" },
|
|
7656
7657
|
help: { type: "boolean", short: "h" },
|
|
7657
|
-
version: { type: "boolean", short: "v" }
|
|
7658
|
+
version: { type: "boolean", short: "v" },
|
|
7659
|
+
verbose: { type: "boolean" }
|
|
7658
7660
|
};
|
|
7659
7661
|
var COMMAND_OPTIONS = {
|
|
7660
7662
|
login: {
|
|
@@ -7753,6 +7755,7 @@ var USAGE = `Usage: amdahl <command> [options]
|
|
|
7753
7755
|
What you can do:
|
|
7754
7756
|
amdahl Set up Claude Code, Cursor or Codex, in a terminal: it asks
|
|
7755
7757
|
which, signs you in, and runs install for each one
|
|
7758
|
+
(--verbose shows the files and profile it used)
|
|
7756
7759
|
amdahl optimize draft.md Rewrite an outbound email or LinkedIn message
|
|
7757
7760
|
amdahl status Check that this workspace is set up to optimize
|
|
7758
7761
|
amdahl keys create --name ci Make an API key for a server or CI
|
|
@@ -7793,6 +7796,7 @@ Global options:
|
|
|
7793
7796
|
--api-key <key> Use an API key for this command (prefer AMDAHL_KEY: argv is visible to other users)
|
|
7794
7797
|
--json Data on stdout as JSON, messages on stderr
|
|
7795
7798
|
--no-color No colour (NO_COLOR is honoured too)
|
|
7799
|
+
--verbose Say more: files written, the profile, where the sign-in is stored
|
|
7796
7800
|
-h, --help Show this help
|
|
7797
7801
|
-v, --version Show the version
|
|
7798
7802
|
`;
|
|
@@ -8087,6 +8091,46 @@ function authorizeUrl(endpoints, input) {
|
|
|
8087
8091
|
const query = params.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
8088
8092
|
return `${endpoints.authorization_endpoint}?${query}`;
|
|
8089
8093
|
}
|
|
8094
|
+
var SIGN_IN_AGAIN = {
|
|
8095
|
+
not_work_email: { why: "Amdahl needs your work email.", label: "Sign in with your work email" },
|
|
8096
|
+
subdomain_email: { why: "Use your main company email.", label: "Sign in again" },
|
|
8097
|
+
email_unconfirmed: { why: "Check your inbox to confirm your email, then try again.", label: "Try again" }
|
|
8098
|
+
};
|
|
8099
|
+
function signInAgainLine(reason, url) {
|
|
8100
|
+
const again = SIGN_IN_AGAIN[reason];
|
|
8101
|
+
return again ? `${again.why} ${again.label}:
|
|
8102
|
+
|
|
8103
|
+
${url}
|
|
8104
|
+
` : "";
|
|
8105
|
+
}
|
|
8106
|
+
function noBusinessMessage(consoleUrl, mayCreate, reason) {
|
|
8107
|
+
switch (reason) {
|
|
8108
|
+
case void 0:
|
|
8109
|
+
return mayCreate ? `You're signed in, but you don't have a workspace yet. Create one at ${consoleUrl}/new, then run \`amdahl login\` again.` : `You're signed in, but you don't have access yet. Amdahl is in beta: join the waitlist at ${consoleUrl}/new, then run \`amdahl login\` again once you're in.`;
|
|
8110
|
+
case "domain_has_workspace":
|
|
8111
|
+
return `Your company already has an Amdahl workspace. Ask its admins to let you in at ${consoleUrl}, then try again.`;
|
|
8112
|
+
case "invite_only":
|
|
8113
|
+
return "Your company's Amdahl workspace is invite-only. Ask a colleague in it to invite you, then try again.";
|
|
8114
|
+
case "not_work_email":
|
|
8115
|
+
case "subdomain_email":
|
|
8116
|
+
case "email_unconfirmed": {
|
|
8117
|
+
const again = SIGN_IN_AGAIN[reason];
|
|
8118
|
+
return `${again.why} Run this again, then choose "${again.label}" in the browser.`;
|
|
8119
|
+
}
|
|
8120
|
+
case "daily_cap":
|
|
8121
|
+
return "Amdahl isn't taking more new workspaces today. Try again tomorrow.";
|
|
8122
|
+
case "paused":
|
|
8123
|
+
return "New workspaces are paused for now. Try again later.";
|
|
8124
|
+
case "domain_used":
|
|
8125
|
+
return "Your company already used its free workspace. Email hello@amdahl.ai to get access.";
|
|
8126
|
+
default:
|
|
8127
|
+
return "Amdahl couldn't create your workspace just now. Try again in a few minutes.";
|
|
8128
|
+
}
|
|
8129
|
+
}
|
|
8130
|
+
function signInAgainUrl(authorize) {
|
|
8131
|
+
const url = new URL(authorize);
|
|
8132
|
+
return `${url.origin}/login?redirect=${encodeURIComponent(`${url.pathname}${url.search}`)}`;
|
|
8133
|
+
}
|
|
8090
8134
|
function html(text) {
|
|
8091
8135
|
return text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
8092
8136
|
}
|
|
@@ -8094,6 +8138,8 @@ function page(body) {
|
|
|
8094
8138
|
return `<!doctype html><html><head><meta charset="utf-8"><title>Amdahl CLI</title></head><body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;line-height:1.5">${body}</body></html>`;
|
|
8095
8139
|
}
|
|
8096
8140
|
async function startLoopback(io, input) {
|
|
8141
|
+
let authorize = null;
|
|
8142
|
+
let refused = null;
|
|
8097
8143
|
let settle;
|
|
8098
8144
|
let fail;
|
|
8099
8145
|
const result = new Promise((resolve, reject) => {
|
|
@@ -8120,9 +8166,18 @@ async function startLoopback(io, input) {
|
|
|
8120
8166
|
const error = url.searchParams.get("error");
|
|
8121
8167
|
const code = url.searchParams.get("code");
|
|
8122
8168
|
if (error === "no_business") {
|
|
8123
|
-
const
|
|
8124
|
-
|
|
8125
|
-
|
|
8169
|
+
const mayCreate = url.searchParams.get("may_create") === "true";
|
|
8170
|
+
const reason = url.searchParams.get("reason") ?? void 0;
|
|
8171
|
+
const again = reason ? SIGN_IN_AGAIN[reason] : void 0;
|
|
8172
|
+
if (again && authorize) {
|
|
8173
|
+
refused = reason;
|
|
8174
|
+
const retry = signInAgainUrl(authorize);
|
|
8175
|
+
send2(200, `<p>${html(again.why)}</p><p><a href="${html(retry)}">${html(again.label)} \u2192</a></p>`);
|
|
8176
|
+
input.onSignInAgain?.(reason, retry);
|
|
8177
|
+
return;
|
|
8178
|
+
}
|
|
8179
|
+
send2(200, `<p>${html(noBusinessMessage(input.consoleUrl, mayCreate, reason))}</p>`);
|
|
8180
|
+
settle({ kind: "no_business", mayCreate, reason });
|
|
8126
8181
|
} else if (error) {
|
|
8127
8182
|
send2(400, `<p>Sign-in failed: ${html(url.searchParams.get("error_description") ?? error)}</p>`);
|
|
8128
8183
|
settle({ kind: "error", error, description: url.searchParams.get("error_description") ?? void 0 });
|
|
@@ -8150,12 +8205,17 @@ async function startLoopback(io, input) {
|
|
|
8150
8205
|
});
|
|
8151
8206
|
const port = server.address().port;
|
|
8152
8207
|
const timer = setTimeout(() => {
|
|
8153
|
-
fail(
|
|
8208
|
+
fail(
|
|
8209
|
+
refused ? new CliError("no_business", noBusinessMessage(input.consoleUrl, false, refused), EXIT.noWorkspace) : new CliError("login_timeout", "Timed out waiting for the browser sign-in.", EXIT.general, "Run `amdahl login` again.")
|
|
8210
|
+
);
|
|
8154
8211
|
}, input.timeoutMs);
|
|
8155
8212
|
timer.unref?.();
|
|
8156
8213
|
return {
|
|
8157
8214
|
redirectUri: `http://127.0.0.1:${port}/callback`,
|
|
8158
8215
|
result,
|
|
8216
|
+
setAuthorizeUrl: (url) => {
|
|
8217
|
+
authorize = url;
|
|
8218
|
+
},
|
|
8159
8219
|
close: () => {
|
|
8160
8220
|
clearTimeout(timer);
|
|
8161
8221
|
server.closeAllConnections?.();
|
|
@@ -8173,7 +8233,7 @@ function consoleUrlFor(io, apiUrl) {
|
|
|
8173
8233
|
}
|
|
8174
8234
|
|
|
8175
8235
|
// src/secrets.ts
|
|
8176
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync } from "fs";
|
|
8236
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, readFileSync as readFileSync2, rmSync, statSync } from "fs";
|
|
8177
8237
|
import { join as join2 } from "path";
|
|
8178
8238
|
var KEYCHAIN_SERVICE = "amdahl-cli";
|
|
8179
8239
|
function keychainAccount(apiUrl, profile) {
|
|
@@ -8260,10 +8320,28 @@ function readCredentials(path) {
|
|
|
8260
8320
|
return { version: 1, accounts: {} };
|
|
8261
8321
|
}
|
|
8262
8322
|
}
|
|
8263
|
-
function fileStore(io,
|
|
8323
|
+
function fileStore(io, note) {
|
|
8264
8324
|
const path = credentialsPath(io);
|
|
8325
|
+
let checked = false;
|
|
8326
|
+
const checkMode = () => {
|
|
8327
|
+
if (checked || io.platform === "win32" || !existsSync2(path)) return;
|
|
8328
|
+
checked = true;
|
|
8329
|
+
if ((statSync(path).mode & 63) === 0) return;
|
|
8330
|
+
try {
|
|
8331
|
+
chmodSync2(path, 384);
|
|
8332
|
+
} catch {
|
|
8333
|
+
io.stderr(`Warning: ${path} can be read by other users on this machine, and this CLI cannot change that. Run \`chmod 600 ${path}\` as its owner.
|
|
8334
|
+
`);
|
|
8335
|
+
return;
|
|
8336
|
+
}
|
|
8337
|
+
io.stderr(
|
|
8338
|
+
`Warning: ${path} was readable by other users on this machine. It is private now; to be safe, run \`amdahl logout\` and sign in again.
|
|
8339
|
+
`
|
|
8340
|
+
);
|
|
8341
|
+
};
|
|
8265
8342
|
return {
|
|
8266
8343
|
async get(account) {
|
|
8344
|
+
checkMode();
|
|
8267
8345
|
return asSecrets(readCredentials(path).accounts[account]);
|
|
8268
8346
|
},
|
|
8269
8347
|
async set(account, secrets) {
|
|
@@ -8271,11 +8349,9 @@ function fileStore(io, warn) {
|
|
|
8271
8349
|
const file = readCredentials(path);
|
|
8272
8350
|
file.accounts[account] = secrets;
|
|
8273
8351
|
writePrivateJson(path, file);
|
|
8274
|
-
if (!existed &&
|
|
8275
|
-
io.stderr(
|
|
8276
|
-
|
|
8277
|
-
`
|
|
8278
|
-
);
|
|
8352
|
+
if (!existed && note) {
|
|
8353
|
+
io.stderr(`No OS keychain here, so your sign-in is stored in ${path} (readable only by you).
|
|
8354
|
+
`);
|
|
8279
8355
|
}
|
|
8280
8356
|
},
|
|
8281
8357
|
async delete(account) {
|
|
@@ -8287,7 +8363,7 @@ function fileStore(io, warn) {
|
|
|
8287
8363
|
}
|
|
8288
8364
|
};
|
|
8289
8365
|
}
|
|
8290
|
-
function createSecretStore(io) {
|
|
8366
|
+
function createSecretStore(io, options = {}) {
|
|
8291
8367
|
const forced = io.env.AMDAHL_CREDENTIAL_STORE;
|
|
8292
8368
|
if (forced && forced !== "keychain" && forced !== "file") {
|
|
8293
8369
|
throw new CliError(
|
|
@@ -8314,7 +8390,7 @@ function createSecretStore(io) {
|
|
|
8314
8390
|
}
|
|
8315
8391
|
};
|
|
8316
8392
|
}
|
|
8317
|
-
const file = fileStore(io, true);
|
|
8393
|
+
const file = fileStore(io, options.verbose === true);
|
|
8318
8394
|
if (!keychain) return file;
|
|
8319
8395
|
return {
|
|
8320
8396
|
async get(account) {
|
|
@@ -8366,7 +8442,7 @@ function createCtx(io, out, flags) {
|
|
|
8366
8442
|
profile,
|
|
8367
8443
|
apiUrl: normaliseApiUrl(rawApi),
|
|
8368
8444
|
nonInteractive: !io.stdinIsTTY || io.env.AMDAHL_NO_PROMPT === "1",
|
|
8369
|
-
secrets: () => store ??= createSecretStore(io),
|
|
8445
|
+
secrets: () => store ??= createSecretStore(io, { verbose: flags.verbose === true }),
|
|
8370
8446
|
saveConfig: () => saveConfig(io, config)
|
|
8371
8447
|
};
|
|
8372
8448
|
}
|
|
@@ -8464,15 +8540,45 @@ async function call(fn) {
|
|
|
8464
8540
|
}
|
|
8465
8541
|
}
|
|
8466
8542
|
|
|
8543
|
+
// src/output.ts
|
|
8544
|
+
var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
8545
|
+
function printable(text) {
|
|
8546
|
+
return text.replace(CONTROL_RE, "");
|
|
8547
|
+
}
|
|
8548
|
+
function createOutput(io, json, noColor) {
|
|
8549
|
+
const color = !noColor && !io.env.NO_COLOR && io.env.TERM !== "dumb" && io.stderrIsTTY && !json;
|
|
8550
|
+
return {
|
|
8551
|
+
json,
|
|
8552
|
+
color,
|
|
8553
|
+
result(data, human) {
|
|
8554
|
+
if (json) {
|
|
8555
|
+
io.stdout(`${JSON.stringify(data)}
|
|
8556
|
+
`);
|
|
8557
|
+
} else if (human !== void 0) {
|
|
8558
|
+
io.stdout(human.endsWith("\n") ? human : `${human}
|
|
8559
|
+
`);
|
|
8560
|
+
}
|
|
8561
|
+
},
|
|
8562
|
+
info(text) {
|
|
8563
|
+
io.stderr(text.endsWith("\n") ? text : `${text}
|
|
8564
|
+
`);
|
|
8565
|
+
},
|
|
8566
|
+
event(data) {
|
|
8567
|
+
if (json) io.stderr(`${JSON.stringify(data)}
|
|
8568
|
+
`);
|
|
8569
|
+
},
|
|
8570
|
+
bold(text) {
|
|
8571
|
+
return color ? `\x1B[1m${text}\x1B[22m` : text;
|
|
8572
|
+
}
|
|
8573
|
+
};
|
|
8574
|
+
}
|
|
8575
|
+
|
|
8467
8576
|
// src/commands/auth.ts
|
|
8468
8577
|
var PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
8469
|
-
function noBusinessMessage(consoleUrl, mayCreate) {
|
|
8470
|
-
return mayCreate ? `You're signed in, but you don't have a workspace yet. Create one at ${consoleUrl}/new, then run \`amdahl login\` again.` : `You're signed in, but you don't have access yet. Amdahl is in beta: join the waitlist at ${consoleUrl}/new, then run \`amdahl login\` again once you're in.`;
|
|
8471
|
-
}
|
|
8472
8578
|
function tokenPreview(token) {
|
|
8473
8579
|
return `${token.slice(0, 10)}...`;
|
|
8474
8580
|
}
|
|
8475
|
-
async function
|
|
8581
|
+
async function browserSignIn(ctx) {
|
|
8476
8582
|
const { io, out, flags } = ctx;
|
|
8477
8583
|
const workspace = str(flags.workspace);
|
|
8478
8584
|
const rawPort = str(flags.port);
|
|
@@ -8487,35 +8593,52 @@ async function login(ctx) {
|
|
|
8487
8593
|
const endpoints = await discover(ctx.apiUrl);
|
|
8488
8594
|
const pkce = createPkce();
|
|
8489
8595
|
const consoleUrl = consoleUrlFor(io, ctx.apiUrl);
|
|
8596
|
+
let timer;
|
|
8597
|
+
let linkShown = false;
|
|
8490
8598
|
const loopback = await startLoopback(io, {
|
|
8491
8599
|
port,
|
|
8492
8600
|
state: pkce.state,
|
|
8493
8601
|
consoleUrl,
|
|
8494
|
-
timeoutMs: io.loginTimeoutMs
|
|
8602
|
+
timeoutMs: io.loginTimeoutMs,
|
|
8603
|
+
// The page carries the way on; the authorize link would only sign in as the same address again.
|
|
8604
|
+
onSignInAgain: (reason, retry) => {
|
|
8605
|
+
clearTimeout(timer);
|
|
8606
|
+
linkShown = true;
|
|
8607
|
+
out.info(signInAgainLine(reason, retry));
|
|
8608
|
+
}
|
|
8495
8609
|
});
|
|
8496
8610
|
try {
|
|
8497
8611
|
const url = authorizeUrl(endpoints, { redirectUri: loopback.redirectUri, pkce, workspace });
|
|
8612
|
+
loopback.setAuthorizeUrl(url);
|
|
8498
8613
|
out.event({ event: "open_url", url });
|
|
8499
|
-
|
|
8500
|
-
|
|
8614
|
+
const showLink = (lead) => {
|
|
8615
|
+
if (linkShown) return;
|
|
8616
|
+
linkShown = true;
|
|
8617
|
+
out.info(`${lead}
|
|
8501
8618
|
|
|
8502
8619
|
${url}
|
|
8503
8620
|
`);
|
|
8621
|
+
};
|
|
8622
|
+
if (flags["no-browser"]) {
|
|
8623
|
+
showLink("Open this URL in a browser to sign in:");
|
|
8504
8624
|
} else {
|
|
8505
|
-
out.info(
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8625
|
+
out.info("Opening your browser\u2026");
|
|
8626
|
+
void io.openUrl(url).then((opened) => {
|
|
8627
|
+
if (!opened) showLink("Your browser did not open. Open this link to sign in:");
|
|
8628
|
+
});
|
|
8629
|
+
timer = setTimeout(() => showLink("Finish in your browser, or open this link:"), io.browserGraceMs);
|
|
8630
|
+
timer.unref?.();
|
|
8510
8631
|
}
|
|
8511
8632
|
const callback = await loopback.result;
|
|
8633
|
+
clearTimeout(timer);
|
|
8634
|
+
linkShown = true;
|
|
8512
8635
|
if (callback.kind === "no_business") {
|
|
8513
|
-
throw new CliError("no_business", noBusinessMessage(consoleUrl, callback.mayCreate), EXIT.noWorkspace);
|
|
8636
|
+
throw new CliError("no_business", noBusinessMessage(consoleUrl, callback.mayCreate, callback.reason), EXIT.noWorkspace);
|
|
8514
8637
|
}
|
|
8515
8638
|
if (callback.kind === "error") {
|
|
8516
8639
|
throw new CliError(
|
|
8517
8640
|
callback.error,
|
|
8518
|
-
`Sign-in failed: ${callback.description ?? callback.error}`,
|
|
8641
|
+
`Sign-in failed: ${printable(callback.description ?? callback.error)}`,
|
|
8519
8642
|
callback.error === "access_denied" ? EXIT.forbidden : EXIT.general
|
|
8520
8643
|
);
|
|
8521
8644
|
}
|
|
@@ -8539,22 +8662,31 @@ async function login(ctx) {
|
|
|
8539
8662
|
};
|
|
8540
8663
|
ctx.config.default_profile = name;
|
|
8541
8664
|
ctx.saveConfig();
|
|
8542
|
-
|
|
8543
|
-
{
|
|
8544
|
-
ok: true,
|
|
8545
|
-
profile: name,
|
|
8546
|
-
user: { id: me.user_id, email: me.email ?? null },
|
|
8547
|
-
workspace: ws,
|
|
8548
|
-
scopes: me.scopes,
|
|
8549
|
-
api_url: ctx.apiUrl
|
|
8550
|
-
},
|
|
8551
|
-
`Signed in as ${me.email ?? me.user_id} to ${out.bold(ws.name)} (${ws.slug}). Profile: ${name}.`
|
|
8552
|
-
);
|
|
8553
|
-
return EXIT.ok;
|
|
8665
|
+
return { profile: name, me, workspace: ws };
|
|
8554
8666
|
} finally {
|
|
8667
|
+
clearTimeout(timer);
|
|
8555
8668
|
loopback.close();
|
|
8556
8669
|
}
|
|
8557
8670
|
}
|
|
8671
|
+
function signedInLine(ctx, { profile, me, workspace }) {
|
|
8672
|
+
return `Signed in as ${printable(me.email ?? me.user_id)} to ${ctx.out.bold(printable(workspace.name))} (${printable(workspace.slug)}). Profile: ${profile}.`;
|
|
8673
|
+
}
|
|
8674
|
+
async function login(ctx) {
|
|
8675
|
+
const signedIn = await browserSignIn(ctx);
|
|
8676
|
+
const { profile, me, workspace } = signedIn;
|
|
8677
|
+
ctx.out.result(
|
|
8678
|
+
{
|
|
8679
|
+
ok: true,
|
|
8680
|
+
profile,
|
|
8681
|
+
user: { id: me.user_id, email: me.email ?? null },
|
|
8682
|
+
workspace,
|
|
8683
|
+
scopes: me.scopes,
|
|
8684
|
+
api_url: ctx.apiUrl
|
|
8685
|
+
},
|
|
8686
|
+
signedInLine(ctx, signedIn)
|
|
8687
|
+
);
|
|
8688
|
+
return EXIT.ok;
|
|
8689
|
+
}
|
|
8558
8690
|
async function logout(ctx) {
|
|
8559
8691
|
const { config, out } = ctx;
|
|
8560
8692
|
const names = ctx.flags.all ? Object.keys(config.profiles) : ctx.profileName && config.profiles[ctx.profileName] ? [ctx.profileName] : [];
|
|
@@ -8736,14 +8868,16 @@ async function isInstalled(ctx, client) {
|
|
|
8736
8868
|
return false;
|
|
8737
8869
|
}
|
|
8738
8870
|
}
|
|
8871
|
+
async function installClient(ctx, client, print) {
|
|
8872
|
+
const mcp = await installMcp(ctx, client, print);
|
|
8873
|
+
return { mcp, skill: installSkill(ctx, client, print) };
|
|
8874
|
+
}
|
|
8739
8875
|
async function install(ctx, args) {
|
|
8740
8876
|
const client = args[0];
|
|
8741
8877
|
if (!client || args.length > 1 || !CLIENTS.includes(client)) {
|
|
8742
8878
|
throw usageError(`Usage: amdahl install ${CLIENTS.join("|")} [--print] [--force]`);
|
|
8743
8879
|
}
|
|
8744
|
-
const
|
|
8745
|
-
const mcp = await installMcp(ctx, client, print);
|
|
8746
|
-
const skill = installSkill(ctx, client, print);
|
|
8880
|
+
const { mcp, skill } = await installClient(ctx, client, ctx.flags.print === true);
|
|
8747
8881
|
const human = [mcp.human.replace(/\n$/, ""), skill.human].filter(Boolean).join("\n");
|
|
8748
8882
|
ctx.out.result({ ...mcp.data, skill: skill.data }, human);
|
|
8749
8883
|
for (const note of [...mcp.notes, ...skill.notes]) ctx.out.info(note);
|
|
@@ -9120,7 +9254,7 @@ import { dirname as dirname3, join as join6, relative, sep } from "path";
|
|
|
9120
9254
|
|
|
9121
9255
|
// src/commands/skills-install.ts
|
|
9122
9256
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
9123
|
-
import { chmodSync as
|
|
9257
|
+
import { chmodSync as chmodSync3, lstatSync, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
9124
9258
|
import { dirname as dirname2, join as join5 } from "path";
|
|
9125
9259
|
function skillDirs(targets, name) {
|
|
9126
9260
|
const dirs = [];
|
|
@@ -9237,7 +9371,7 @@ function stage(target, name, archive, staged) {
|
|
|
9237
9371
|
s.createdParent = mkdirSync3(dirname2(target), { recursive: true }) ?? void 0;
|
|
9238
9372
|
refuseLinks(target);
|
|
9239
9373
|
s.temp = mkdtempSync(join5(dirname2(target), `.${name}.amdahl-tmp-`));
|
|
9240
|
-
|
|
9374
|
+
chmodSync3(s.temp, 493);
|
|
9241
9375
|
for (const dir of archive.dirs) mkdirSync3(join5(s.temp, dir));
|
|
9242
9376
|
for (const file of archive.files) writeFileSync3(join5(s.temp, file.path), file.bytes, { flag: "wx", mode: file.mode });
|
|
9243
9377
|
}
|
|
@@ -9398,9 +9532,11 @@ var SHOWN = [
|
|
|
9398
9532
|
{ client: "cursor", name: "Cursor" },
|
|
9399
9533
|
{ client: "codex", name: "Codex" }
|
|
9400
9534
|
];
|
|
9401
|
-
function tryItLine(name) {
|
|
9535
|
+
function tryItLine(name, connect = false) {
|
|
9402
9536
|
const ask2 = 'say "make this email sound less like AI"';
|
|
9403
|
-
|
|
9537
|
+
if (connect && name === "Claude Code") return `Try it: in Claude Code, run /mcp once to connect, then ${ask2}`;
|
|
9538
|
+
if (name === "Codex") return `Try it: run \`codex mcp login amdahl\`, then open Codex and ${ask2}`;
|
|
9539
|
+
return `Try it: open ${name} and ${ask2}`;
|
|
9404
9540
|
}
|
|
9405
9541
|
function shouldSetUp(io) {
|
|
9406
9542
|
const { env } = io;
|
|
@@ -9417,7 +9553,7 @@ async function signIn(ctx) {
|
|
|
9417
9553
|
}
|
|
9418
9554
|
try {
|
|
9419
9555
|
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 };
|
|
9556
|
+
return { kind: "in", email: printable(me.email ?? me.user_id), workspace: printable(me.workspace?.name ?? me.business_id) };
|
|
9421
9557
|
} catch (err) {
|
|
9422
9558
|
if (!(err instanceof CliError && err.exit === EXIT.unauthenticated)) throw err;
|
|
9423
9559
|
return credential.source === "profile" ? { kind: "out" } : { kind: "rejected", source: credential.source };
|
|
@@ -9425,9 +9561,13 @@ async function signIn(ctx) {
|
|
|
9425
9561
|
}
|
|
9426
9562
|
async function setup(ctx) {
|
|
9427
9563
|
const { io, out } = ctx;
|
|
9564
|
+
const verbose = ctx.flags.verbose === true;
|
|
9428
9565
|
const say = (line) => io.stdout(`${line}
|
|
9429
9566
|
`);
|
|
9430
|
-
|
|
9567
|
+
const detail = (text) => {
|
|
9568
|
+
if (verbose && text) say(text.replace(/^/gm, " "));
|
|
9569
|
+
};
|
|
9570
|
+
say(`${out.bold("Amdahl")} \xB7 predictive ML for GTM`);
|
|
9431
9571
|
const found = await detectClients(ctx);
|
|
9432
9572
|
const detected = SHOWN.filter((c) => found[c.client]);
|
|
9433
9573
|
let who;
|
|
@@ -9448,31 +9588,33 @@ async function setup(ctx) {
|
|
|
9448
9588
|
}
|
|
9449
9589
|
}
|
|
9450
9590
|
}
|
|
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
9591
|
const picks = await io.multiSelect(
|
|
9456
|
-
"Set up
|
|
9592
|
+
"Set up in:",
|
|
9457
9593
|
SHOWN.map((c) => c.name),
|
|
9458
9594
|
{ ticked: SHOWN.map((c) => found[c.client]), color: out.color }
|
|
9459
9595
|
);
|
|
9460
9596
|
if (picks.length === 0) {
|
|
9461
|
-
say("Nothing picked, so nothing changed.
|
|
9597
|
+
say("Nothing picked, so nothing changed.");
|
|
9462
9598
|
return EXIT.ok;
|
|
9463
9599
|
}
|
|
9464
9600
|
who ??= await signIn(ctx);
|
|
9465
|
-
if (who.kind === "in")
|
|
9466
|
-
|
|
9467
|
-
|
|
9601
|
+
if (who.kind === "in") {
|
|
9602
|
+
say(`\u2713 Signed in \xB7 ${who.workspace}`);
|
|
9603
|
+
detail(`Signed in as ${who.email}.`);
|
|
9604
|
+
} else if (who.kind === "out") {
|
|
9605
|
+
const signedIn = await browserSignIn(ctx);
|
|
9606
|
+
say(`\u2713 Signed in \xB7 ${printable(signedIn.workspace.name)}`);
|
|
9607
|
+
detail(signedInLine(ctx, signedIn));
|
|
9608
|
+
} else {
|
|
9468
9609
|
const name = who.source === "flag" ? "--api-key" : who.source.slice("env:".length);
|
|
9469
9610
|
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
9611
|
}
|
|
9471
9612
|
let exit = EXIT.ok;
|
|
9472
9613
|
let ready = null;
|
|
9473
9614
|
for (const { client, name } of picks.flatMap((i) => SHOWN[i] ?? [])) {
|
|
9615
|
+
let steps;
|
|
9474
9616
|
try {
|
|
9475
|
-
await
|
|
9617
|
+
steps = await installClient(ctx, client, false);
|
|
9476
9618
|
} catch (raw) {
|
|
9477
9619
|
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
9620
|
io.stderr(`Error: ${err.message}
|
|
@@ -9481,9 +9623,18 @@ ${err.hint ? `${err.hint}
|
|
|
9481
9623
|
if (exit === EXIT.ok) exit = err.exit;
|
|
9482
9624
|
continue;
|
|
9483
9625
|
}
|
|
9484
|
-
|
|
9626
|
+
const action = steps.mcp.data.action;
|
|
9627
|
+
if (action === "printed") {
|
|
9628
|
+
say(`! ${name}: \`claude\` is not on your PATH. Once it is, run:
|
|
9629
|
+
${String(steps.mcp.data.command)}`);
|
|
9630
|
+
} else {
|
|
9631
|
+
say(action === "unchanged" ? `\u2713 ${name} already has Amdahl` : `\u2713 Added to ${name}`);
|
|
9632
|
+
ready ??= { name, connect: action !== "unchanged" };
|
|
9633
|
+
}
|
|
9634
|
+
const mcpDetail = action === "printed" ? [] : [steps.mcp.human, ...steps.mcp.notes];
|
|
9635
|
+
detail([...mcpDetail, steps.skill.human, ...steps.skill.notes].filter(Boolean).join("\n"));
|
|
9485
9636
|
}
|
|
9486
|
-
if (ready) say(tryItLine(ready));
|
|
9637
|
+
if (ready) say(tryItLine(ready.name, ready.connect));
|
|
9487
9638
|
return exit;
|
|
9488
9639
|
}
|
|
9489
9640
|
|
|
@@ -9491,39 +9642,6 @@ ${err.hint ? `${err.hint}
|
|
|
9491
9642
|
var import_yaml = __toESM(require_dist(), 1);
|
|
9492
9643
|
import { createHash as createHash4 } from "crypto";
|
|
9493
9644
|
|
|
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
9645
|
// src/tar-gz.ts
|
|
9528
9646
|
import { inflateRawSync } from "zlib";
|
|
9529
9647
|
var MAX_ARCHIVE_FILES = 200;
|
|
@@ -10130,7 +10248,7 @@ async function workspaceUse(ctx, args) {
|
|
|
10130
10248
|
}
|
|
10131
10249
|
|
|
10132
10250
|
// src/version.ts
|
|
10133
|
-
var VERSION = "0.3.
|
|
10251
|
+
var VERSION = "0.3.1";
|
|
10134
10252
|
|
|
10135
10253
|
// src/main.ts
|
|
10136
10254
|
var COMMANDS = {
|
|
@@ -10165,7 +10283,7 @@ async function runCli(argv, io) {
|
|
|
10165
10283
|
return EXIT.ok;
|
|
10166
10284
|
}
|
|
10167
10285
|
if (!parsed.command) {
|
|
10168
|
-
if (argv.
|
|
10286
|
+
if (argv.every((a) => a === "--verbose") && shouldSetUp(io)) return await setup(createCtx(io, out, parsed.flags));
|
|
10169
10287
|
io.stderr(USAGE);
|
|
10170
10288
|
return EXIT.usage;
|
|
10171
10289
|
}
|
package/package.json
CHANGED