@amdahl/cli 0.3.4 → 0.3.5
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 +162 -66
- package/dist/amdahl.js +377 -100
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,93 +45,172 @@ anything, names the version it found, and links the Node.js download page.
|
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
47
|
# Run once, with no install
|
|
48
|
-
npx
|
|
48
|
+
npx -y amdahl
|
|
49
49
|
|
|
50
50
|
# Or install the `amdahl` command
|
|
51
51
|
npm i -g @amdahl/cli
|
|
52
|
-
amdahl
|
|
52
|
+
amdahl
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
`amdahl` is also on npm by its short name, so `npx amdahl ...` is the same as
|
|
56
56
|
`npx @amdahl/cli ...`. That package ([packages/amdahl](packages/amdahl)) has no
|
|
57
57
|
code of its own: it depends on `@amdahl/cli` at the same version and runs it.
|
|
58
|
+
npm asks "Ok to proceed? (y)" before every `npx` run of a package you have not
|
|
59
|
+
installed; `npx -y` skips that question, so every example here uses it.
|
|
58
60
|
|
|
59
|
-
The quickest way to start is `npx amdahl` with nothing after it (or
|
|
60
|
-
`npx amdahl setup`), in a terminal. It
|
|
61
|
+
The quickest way to start is `npx -y amdahl` with nothing after it (or
|
|
62
|
+
`npx -y amdahl setup`), in a terminal. It asks where Amdahl should work:
|
|
61
63
|
|
|
62
64
|
```
|
|
63
|
-
$ npx amdahl
|
|
65
|
+
$ npx -y amdahl
|
|
64
66
|
Amdahl · predictive ML for GTM
|
|
65
67
|
Where should Amdahl work?
|
|
66
68
|
> [x] Claude Code found on your machine
|
|
67
69
|
[ ] Cursor
|
|
68
70
|
[ ] Codex
|
|
69
|
-
|
|
71
|
+
[ ] This terminal
|
|
72
|
+
↑↓ move · Space tick/untick · Enter continue
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
It finds Claude Code, Cursor and Codex by their command on your PATH
|
|
73
76
|
(`claude`, `cursor`, `codex`) or their files in your home folder (`.claude`
|
|
74
77
|
or `.claude.json`, `.cursor`, `.codex`). Only the ones it finds are ticked
|
|
75
|
-
and tagged "found on your machine" (in dim grey where colour is allowed).
|
|
76
|
-
|
|
77
|
-
the
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
and tagged "found on your machine" (in dim grey where colour is allowed).
|
|
79
|
+
"This terminal" is always there and never ticked for you. If it finds none
|
|
80
|
+
of the three, it says so under the question, points Claude and ChatGPT app
|
|
81
|
+
users to https://amdahl.ai/connect, and the marker starts on "This
|
|
82
|
+
terminal", so Space then Enter picks it:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
Where should Amdahl work?
|
|
86
|
+
We didn't find Claude Code, Cursor or Codex on this computer.
|
|
87
|
+
Using the Claude or ChatGPT app? Connect it at https://amdahl.ai/connect
|
|
88
|
+
[ ] Claude Code
|
|
89
|
+
[ ] Cursor
|
|
90
|
+
[ ] Codex
|
|
91
|
+
> [ ] This terminal
|
|
92
|
+
↑↓ move · Space tick/untick · Enter continue
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The `>` marker starts on the first ticked row, and only the marker is
|
|
96
|
+
coloured, so the row under it never looks picked when it is not. Esc, `q`
|
|
97
|
+
or Ctrl+C stops with nothing changed (exit 130). The menu redraws in place
|
|
98
|
+
in any width, also in a split pane narrower than its lines, and `NO_COLOR`
|
|
99
|
+
or `--no-color` turn the colour off. Enter with nothing ticked changes
|
|
100
|
+
nothing and says how to try Amdahl: run it again and tick "This terminal",
|
|
101
|
+
or connect the Claude or ChatGPT app at https://amdahl.ai/connect.
|
|
102
|
+
|
|
103
|
+
### The AI tools: set up first, then one sign-in in the tool
|
|
104
|
+
|
|
105
|
+
For Claude Code, Cursor and Codex there is no sign-in in the terminal. The
|
|
106
|
+
setup writes each tool's config straight away (as `amdahl install` does), so
|
|
107
|
+
nothing is lost if you stop later, and then prints that tool's next steps.
|
|
108
|
+
Each tool signs in to Amdahl itself, over OAuth, the first time it connects;
|
|
109
|
+
that sign-in is also where a new user creates their free workspace. Each
|
|
110
|
+
screen is six lines or fewer:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
✓ Added Amdahl to Claude Code.
|
|
114
|
+
One step left, in Claude Code:
|
|
115
|
+
1. If it's open, quit and reopen it. Type /mcp, pick amdahl, sign in.
|
|
116
|
+
2. Paste an email and say: "Use Amdahl to make this sound less like AI"
|
|
117
|
+
New to Amdahl? The sign-in sets up your free workspace.
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
✓ Added Amdahl to Cursor.
|
|
122
|
+
One step left, in Cursor:
|
|
123
|
+
1. If it's open, quit and reopen it. In Customize > MCPs, sign in to amdahl.
|
|
124
|
+
2. In chat, paste an email, say: "Use Amdahl to make this sound less like AI"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Cursor reads `~/.cursor/mcp.json` when it starts and lists its MCP servers
|
|
128
|
+
under Customize, then MCPs.
|
|
129
|
+
|
|
130
|
+
Codex runs in the same terminal, so with `codex` on your PATH the setup
|
|
131
|
+
offers to run its sign-in for you:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
✓ Added Amdahl to Codex.
|
|
135
|
+
Press Enter to sign Codex in to Amdahl (opens your browser).
|
|
136
|
+
✓ Codex is connected.
|
|
137
|
+
Run codex, paste an email and say: "Use Amdahl to make this sound less like AI"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Enter runs `codex mcp login amdahl` on your terminal (it prints the link and
|
|
141
|
+
opens the browser). Ctrl+C there skips it and names the command to run
|
|
142
|
+
later; if the sign-in does not finish, it says so and names the same
|
|
143
|
+
command. Without `codex` on your PATH, the screen lists `codex mcp login
|
|
144
|
+
amdahl` as step 1. With more than one tool, the screens follow one another
|
|
145
|
+
and the "New to Amdahl?" line is printed once. A tool that fails (a broken
|
|
146
|
+
`~/.cursor/mcp.json` or `~/.claude.json`, say) prints its error, the rest
|
|
147
|
+
still run, and the exit code is the first failure's. In a terminal narrower
|
|
148
|
+
than a screen's lines, they break between words, and a numbered step's next
|
|
149
|
+
line starts under its text.
|
|
150
|
+
|
|
151
|
+
### This terminal: try it on a message
|
|
152
|
+
|
|
153
|
+
Pick "This terminal" (alone or beside the tools, which are set up first) to
|
|
154
|
+
see Amdahl work right away. It signs you in through the browser if you are
|
|
155
|
+
not signed in yet, then asks for a message:
|
|
86
156
|
|
|
87
157
|
```
|
|
88
|
-
Where should Amdahl work? Claude Code
|
|
89
|
-
Opening your browser…
|
|
90
158
|
✓ Signed in · Acme
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
159
|
+
Paste an email or LinkedIn message, then press Enter on an empty line:
|
|
160
|
+
<your draft>
|
|
161
|
+
Amdahl is rewriting it. This can take a minute.
|
|
162
|
+
── Amdahl's version ──────────────
|
|
163
|
+
<the rewritten message>
|
|
164
|
+
Cut the opener and 2 sentences nothing backed.
|
|
165
|
+
Next time: npx -y amdahl optimize draft.txt,
|
|
166
|
+
or pbpaste | npx -y amdahl optimize -
|
|
94
167
|
```
|
|
95
168
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
169
|
+
Empty lines inside a paste stay in the message: the terminal marks a paste
|
|
170
|
+
(bracketed paste), and where it does not, an empty line that more text
|
|
171
|
+
follows at once counts as pasted. Ctrl+D also ends the message. The lines
|
|
172
|
+
under Amdahl's version say what it did, as `amdahl optimize` does. The
|
|
173
|
+
`pbpaste` line is printed on macOS only.
|
|
174
|
+
|
|
175
|
+
While it waits for the browser, it prints one short line under "Opening
|
|
176
|
+
your browser…". In a terminal that shows links (iTerm2, VS Code, Warp,
|
|
177
|
+
Terminal, WezTerm, Ghostty, Windows Terminal, VTE 0.50 or later) it reads
|
|
178
|
+
"Browser didn't open? Open the sign-in page" and the last words are a link;
|
|
179
|
+
elsewhere it says to press Enter for the link. `FORCE_HYPERLINK=1` or `0`
|
|
180
|
+
overrides the guess, and `NO_COLOR` turns the link off. Either way, Enter
|
|
181
|
+
prints the full sign-in link once. Once you are signed in, that line is
|
|
182
|
+
replaced by `✓ Signed in`. If the browser could not be opened, or with
|
|
183
|
+
`--no-browser`, the full link prints at once. It waits up to 15 minutes, so
|
|
184
|
+
someone new can create their workspace on the way and land back in the same
|
|
185
|
+
setup. If the server rejects a key set in `AMDAHL_KEY` (or `AMDAHL_API_KEY`,
|
|
186
|
+
`AMDAHL_ACCESS_TOKEN`, `--api-key`), it says so, skips the browser sign-in
|
|
187
|
+
(that key would still win) and exits 3. Ctrl+C during the sign-in or the
|
|
188
|
+
paste stops with exit 130; if tools were set up first, it says they still
|
|
189
|
+
are.
|
|
190
|
+
|
|
191
|
+
### Run it again
|
|
192
|
+
|
|
193
|
+
When every tool it finds already points at Amdahl, it asks nothing, needs no
|
|
194
|
+
network and signs in nowhere. It says so, with the one thing to try and how
|
|
195
|
+
to finish the tool's sign-in if you have not yet:
|
|
116
196
|
|
|
117
197
|
```
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
198
|
+
Amdahl is in Claude Code. You're all set.
|
|
199
|
+
Paste an email there and say: "Use Amdahl to make this sound less like AI"
|
|
200
|
+
If you've already run /mcp, you're done.
|
|
201
|
+
If not: quit and reopen Claude Code, type /mcp, pick amdahl, sign in.
|
|
202
|
+
Want to try it here instead? Run: npx -y amdahl optimize
|
|
123
203
|
```
|
|
124
204
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`
|
|
128
|
-
|
|
129
|
-
says so.
|
|
205
|
+
An old copy of the Amdahl skill is brought up to date on the way. A tool it
|
|
206
|
+
finds that does not point at Amdahl yet brings the menu back.
|
|
207
|
+
`npx -y amdahl --verbose` also prints the files it wrote, the profile and
|
|
208
|
+
where the sign-in is stored.
|
|
130
209
|
|
|
131
210
|
### Run by an agent, or with no terminal
|
|
132
211
|
|
|
133
|
-
You can ask Claude Code, Cursor or Codex to run `npx amdahl` for you. With
|
|
134
|
-
no terminal (an agent's shell, a pipe such as `npx amdahl | cat`, or stdin,
|
|
212
|
+
You can ask Claude Code, Cursor or Codex to run `npx -y amdahl` for you. With
|
|
213
|
+
no terminal (an agent's shell, a pipe such as `npx -y amdahl | cat`, or stdin,
|
|
135
214
|
stdout or stderr redirected), and whenever `CLAUDECODE` or `CURSOR_AGENT`
|
|
136
215
|
is set, it sets up without a question and without a browser sign-in:
|
|
137
216
|
|
|
@@ -142,14 +221,22 @@ is set, it sets up without a question and without a browser sign-in:
|
|
|
142
221
|
In Codex's default sandbox, writes outside the workspace and network access
|
|
143
222
|
are blocked, so the run usually fails (`npx` cannot fetch the package, or
|
|
144
223
|
`Could not update ~/.codex/config.toml`, exit 1) unless you approve the
|
|
145
|
-
escalation Codex asks for, or run `npx amdahl` in your own terminal.
|
|
224
|
+
escalation Codex asks for, or run `npx -y amdahl` in your own terminal.
|
|
146
225
|
|
|
147
226
|
Each client signs in on its first connect, over OAuth, so the CLI needs no
|
|
148
|
-
sign-in of its own here. The last lines say
|
|
149
|
-
|
|
227
|
+
sign-in of its own here. The last lines say what to try and the one step
|
|
228
|
+
that connects each client:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
✓ Added to Claude Code
|
|
232
|
+
Try it: paste an email and say "Use Amdahl to make this sound less like AI" in Claude Code.
|
|
233
|
+
Before that, restart Claude Code and run /mcp once to connect Amdahl.
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Run from inside Claude Code, the step is "restart Claude Code and run /mcp
|
|
150
237
|
once", since a running session may only see the new server after a
|
|
151
238
|
restart. If
|
|
152
|
-
it finds no client, it says so on stderr, names `npx amdahl install
|
|
239
|
+
it finds no client, it says so on stderr, names `npx -y amdahl install
|
|
153
240
|
claude-code`, writes nothing and exits 1.
|
|
154
241
|
|
|
155
242
|
**Exit code change in 0.3.4.** Before 0.3.4, bare `amdahl` with no terminal
|
|
@@ -163,8 +250,8 @@ line that says why and names `amdahl install`. CI means one of `CI`, `JENKINS_UR
|
|
|
163
250
|
`BUILD_NUMBER` is set together with `JENKINS_HOME`.
|
|
164
251
|
|
|
165
252
|
When the CLI was started by `npx`, every message that names a command says
|
|
166
|
-
`npx amdahl ...` (for example `npx amdahl login`), since an `npx` user
|
|
167
|
-
`amdahl` on their PATH.
|
|
253
|
+
`npx -y amdahl ...` (for example `npx -y amdahl login`), since an `npx` user
|
|
254
|
+
has no `amdahl` on their PATH, and `-y` skips npm's question.
|
|
168
255
|
|
|
169
256
|
To connect one AI client yourself, run `amdahl install claude-code` (or `codex`,
|
|
170
257
|
`cursor`). It adds the Amdahl MCP server and also installs the Amdahl skill,
|
|
@@ -189,7 +276,7 @@ files are also written whole through a temp file. A symbolic link at
|
|
|
189
276
|
`~/.claude.json` is followed to its target, even one not created yet, and
|
|
190
277
|
stays a link. An `mcpServers` that is not a JSON object (an array, say) in
|
|
191
278
|
`~/.claude.json` or `~/.cursor/mcp.json` is left alone with `invalid_config`,
|
|
192
|
-
found before the
|
|
279
|
+
found before the setup writes anything. If it cannot write the client's config file, it
|
|
193
280
|
exits 1 with `write_failed` and names the file. The skill goes to the
|
|
194
281
|
client's user skills folder:
|
|
195
282
|
|
|
@@ -209,11 +296,11 @@ https://docs.amdahl.ai/skills/amdahl/SKILL.md.
|
|
|
209
296
|
## Quick start
|
|
210
297
|
|
|
211
298
|
```sh
|
|
212
|
-
amdahl # guided setup for Claude Code, Cursor and Codex (an agent can run it too)
|
|
299
|
+
amdahl # guided setup for Claude Code, Cursor and Codex, or try it here (an agent can run it too)
|
|
213
300
|
amdahl login # sign in through the browser with your console account
|
|
214
301
|
amdahl whoami # the account, workspace and role you are signed in as
|
|
215
302
|
amdahl status # can this workspace optimize, and what blocks it
|
|
216
|
-
amdahl optimize draft.md # rewrite one message (or pipe it in with
|
|
303
|
+
amdahl optimize draft.md # rewrite one message (or pipe it in with -, or run it bare and paste)
|
|
217
304
|
amdahl install claude-code # connect an AI client and add the Amdahl skill (also codex, cursor)
|
|
218
305
|
amdahl skills add <publisher>/<skill> # add a skill from amdahl.ai/skills to this project
|
|
219
306
|
```
|
|
@@ -227,6 +314,15 @@ terminal keeps waiting for it. Anything
|
|
|
227
314
|
else exits 5 with the one next step (ask your company's admins, try again
|
|
228
315
|
tomorrow, and so on). The CLI itself never creates or joins a workspace.
|
|
229
316
|
|
|
317
|
+
`amdahl login` ends with the next step: `amdahl optimize` to try a pasted
|
|
318
|
+
message, or bare `amdahl` to add Amdahl to Claude Code, Cursor or Codex.
|
|
319
|
+
|
|
320
|
+
`amdahl optimize` with no file, at a terminal, asks you to paste the message
|
|
321
|
+
(as the setup's "This terminal" does) and signs you in through the browser
|
|
322
|
+
first if you are not signed in ("Sign in first, then paste your message."). With stdin piped, or `-`, it reads stdin
|
|
323
|
+
whole as before. With `AMDAHL_NO_PROMPT=1` at a terminal it still says to
|
|
324
|
+
give a file (exit 2).
|
|
325
|
+
|
|
230
326
|
On a server or in CI, skip `login` and set an API key in `AMDAHL_KEY`.
|
|
231
327
|
|
|
232
328
|
## Commands
|
|
@@ -260,9 +356,9 @@ stderr. An unknown option prints `Unknown option --foo.` and `Run \`amdahl
|
|
|
260
356
|
It needs no sign-in.
|
|
261
357
|
|
|
262
358
|
```sh
|
|
263
|
-
npx amdahl skills add <publisher>/<skill>
|
|
359
|
+
npx -y amdahl skills add <publisher>/<skill>
|
|
264
360
|
# the skill's link works too, with or without .md:
|
|
265
|
-
npx amdahl skills add https://amdahl.ai/skills/<publisher>/<skill>
|
|
361
|
+
npx -y amdahl skills add https://amdahl.ai/skills/<publisher>/<skill>
|
|
266
362
|
```
|
|
267
363
|
|
|
268
364
|
In a terminal, it asks one question. It prints `Checking <publisher>/<skill>
|
|
@@ -452,7 +548,7 @@ code shown in the terminal; then the key is printed once and stored nowhere.
|
|
|
452
548
|
| 7 | approval denied, expired or locked out |
|
|
453
549
|
| 8 | network or 5xx |
|
|
454
550
|
| 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) |
|
|
455
|
-
| 130 | stopped with Ctrl+C (or Esc or `q` in a menu) at a question: "Stopped. Nothing changed." |
|
|
551
|
+
| 130 | stopped with Ctrl+C (or Esc or `q` in a menu) at a question: "Stopped. Nothing changed." (in the setup, after tools were set up: "Stopped. Amdahl is still set up in …") |
|
|
456
552
|
|
|
457
553
|
## Development
|
|
458
554
|
|
package/dist/amdahl.js
CHANGED
|
@@ -7367,10 +7367,10 @@ var require_dist = __commonJS({
|
|
|
7367
7367
|
import { spawn } from "child_process";
|
|
7368
7368
|
import { accessSync, constants, statSync } from "fs";
|
|
7369
7369
|
import { homedir, hostname } from "os";
|
|
7370
|
-
import { createInterface } from "readline";
|
|
7370
|
+
import { createInterface as createInterface2 } from "readline";
|
|
7371
7371
|
|
|
7372
7372
|
// src/select.ts
|
|
7373
|
-
import { emitKeypressEvents } from "readline";
|
|
7373
|
+
import { createInterface, emitKeypressEvents } from "readline";
|
|
7374
7374
|
var HINT = "(Use the arrow keys, then Enter)";
|
|
7375
7375
|
var MULTI_HINT = "\u2191\u2193 move \xB7 Space tick/untick \xB7 Enter to accept";
|
|
7376
7376
|
var OFF = "\x1B[0m";
|
|
@@ -7441,7 +7441,7 @@ ${SHOW_CURSOR}`);
|
|
|
7441
7441
|
emitKeypressEvents(input);
|
|
7442
7442
|
input.setRawMode?.(true);
|
|
7443
7443
|
const cols = width();
|
|
7444
|
-
const top = [question, ...note ? wrapWords(
|
|
7444
|
+
const top = [question, ...note ? note.split("\n").flatMap((line) => wrapWords(line, cols)) : []];
|
|
7445
7445
|
headLines = top.reduce((n, line) => n + screenLines(line, cols), 0);
|
|
7446
7446
|
output.write(`${HIDE_CURSOR}${top.map((line) => `${line}
|
|
7447
7447
|
`).join("")}${frame()}`);
|
|
@@ -7497,7 +7497,8 @@ function selectMenu(input, output, question, choices, options = {}) {
|
|
|
7497
7497
|
}
|
|
7498
7498
|
function multiSelectMenu(input, output, question, choices, options = {}) {
|
|
7499
7499
|
const ticked = choices.map((_, i) => options.ticked?.[i] === true);
|
|
7500
|
-
|
|
7500
|
+
const start = Math.min(Math.max(options.initial ?? 0, 0), choices.length - 1);
|
|
7501
|
+
let current = ticked.includes(true) ? ticked.indexOf(true) : start;
|
|
7501
7502
|
const pointer = `${marker(options)}>${OFF}`;
|
|
7502
7503
|
const pad = Math.max(...choices.map((c) => c.length));
|
|
7503
7504
|
const label = (i) => {
|
|
@@ -7568,6 +7569,56 @@ function listenForEnter(input, onEnter, onCancel) {
|
|
|
7568
7569
|
}
|
|
7569
7570
|
return stop;
|
|
7570
7571
|
}
|
|
7572
|
+
var PASTE_ON = "\x1B[?2004h";
|
|
7573
|
+
var PASTE_OFF = "\x1B[?2004l";
|
|
7574
|
+
var PASTE_SETTLE_MS = 150;
|
|
7575
|
+
function readPaste(input, output, question) {
|
|
7576
|
+
output.write(`${question}
|
|
7577
|
+
`);
|
|
7578
|
+
const rl = createInterface({ input, output, terminal: true, prompt: "" });
|
|
7579
|
+
output.write(PASTE_ON);
|
|
7580
|
+
return new Promise((resolve2, reject) => {
|
|
7581
|
+
const lines = [];
|
|
7582
|
+
let pasting = false;
|
|
7583
|
+
let settle;
|
|
7584
|
+
let settled = false;
|
|
7585
|
+
const finish = (outcome) => {
|
|
7586
|
+
if (settled) return;
|
|
7587
|
+
settled = true;
|
|
7588
|
+
clearTimeout(settle);
|
|
7589
|
+
input.removeListener("keypress", onKeypress);
|
|
7590
|
+
output.write(PASTE_OFF);
|
|
7591
|
+
rl.close();
|
|
7592
|
+
if ("text" in outcome) resolve2(outcome.text);
|
|
7593
|
+
else reject(outcome.error);
|
|
7594
|
+
};
|
|
7595
|
+
const done = () => finish({ text: lines.join("\n").trim() });
|
|
7596
|
+
const onKeypress = (_text, key) => {
|
|
7597
|
+
if (key?.name === "paste-start") pasting = true;
|
|
7598
|
+
else if (key?.name === "paste-end") pasting = false;
|
|
7599
|
+
else if (settle && key?.name !== "return" && key?.name !== "enter") {
|
|
7600
|
+
clearTimeout(settle);
|
|
7601
|
+
settle = void 0;
|
|
7602
|
+
}
|
|
7603
|
+
};
|
|
7604
|
+
input.on("keypress", onKeypress);
|
|
7605
|
+
rl.on("line", (line) => {
|
|
7606
|
+
if (line.trim() || pasting) {
|
|
7607
|
+
lines.push(line);
|
|
7608
|
+
return;
|
|
7609
|
+
}
|
|
7610
|
+
if (lines.length === 0) return;
|
|
7611
|
+
lines.push(line);
|
|
7612
|
+
clearTimeout(settle);
|
|
7613
|
+
settle = setTimeout(done, PASTE_SETTLE_MS);
|
|
7614
|
+
});
|
|
7615
|
+
rl.on("SIGINT", () => {
|
|
7616
|
+
output.write("\n");
|
|
7617
|
+
finish({ error: new PromptCancelled() });
|
|
7618
|
+
});
|
|
7619
|
+
rl.on("close", done);
|
|
7620
|
+
});
|
|
7621
|
+
}
|
|
7571
7622
|
|
|
7572
7623
|
// src/io.ts
|
|
7573
7624
|
var PromptCancelled = class extends Error {
|
|
@@ -7601,7 +7652,7 @@ async function readAllStdin() {
|
|
|
7601
7652
|
return Buffer.concat(chunks).toString("utf8");
|
|
7602
7653
|
}
|
|
7603
7654
|
function promptLine(question) {
|
|
7604
|
-
const rl =
|
|
7655
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
7605
7656
|
return new Promise((resolve2, reject) => {
|
|
7606
7657
|
let settled = false;
|
|
7607
7658
|
const cancel = () => {
|
|
@@ -7620,6 +7671,22 @@ function promptLine(question) {
|
|
|
7620
7671
|
});
|
|
7621
7672
|
});
|
|
7622
7673
|
}
|
|
7674
|
+
function runOnTerminal(command, args) {
|
|
7675
|
+
const hold = () => {
|
|
7676
|
+
};
|
|
7677
|
+
process.on("SIGINT", hold);
|
|
7678
|
+
return new Promise((resolve2) => {
|
|
7679
|
+
let child;
|
|
7680
|
+
try {
|
|
7681
|
+
child = spawn(command, args, { stdio: "inherit" });
|
|
7682
|
+
} catch {
|
|
7683
|
+
resolve2(-1);
|
|
7684
|
+
return;
|
|
7685
|
+
}
|
|
7686
|
+
child.on("error", () => resolve2(-1));
|
|
7687
|
+
child.on("close", (code) => resolve2(code ?? -1));
|
|
7688
|
+
}).finally(() => process.off("SIGINT", hold));
|
|
7689
|
+
}
|
|
7623
7690
|
function uninterruptible(fn) {
|
|
7624
7691
|
const hold = () => {
|
|
7625
7692
|
};
|
|
@@ -7653,6 +7720,7 @@ function realIo() {
|
|
|
7653
7720
|
stdoutIsTTY: Boolean(process.stdout.isTTY),
|
|
7654
7721
|
stderrIsTTY: Boolean(process.stderr.isTTY),
|
|
7655
7722
|
readStdin: readAllStdin,
|
|
7723
|
+
readPaste: (question) => readPaste(process.stdin, process.stderr, question),
|
|
7656
7724
|
prompt: promptLine,
|
|
7657
7725
|
select: (question, choices, options) => selectMenu(process.stdin, process.stderr, question, choices, {
|
|
7658
7726
|
...options,
|
|
@@ -7667,6 +7735,7 @@ function realIo() {
|
|
|
7667
7735
|
listenForEnter: (onEnter, onCancel) => listenForEnter(process.stdin, onEnter, onCancel),
|
|
7668
7736
|
uninterruptible,
|
|
7669
7737
|
run: runProcess,
|
|
7738
|
+
runInteractive: runOnTerminal,
|
|
7670
7739
|
openUrl: async (url) => {
|
|
7671
7740
|
const [cmd, args] = openerFor(process.platform, url);
|
|
7672
7741
|
return (await runProcess(cmd, args)).code === 0;
|
|
@@ -7694,7 +7763,7 @@ function startedByNpx(io) {
|
|
|
7694
7763
|
return io.env.npm_command === "exec" || io.env.npm_lifecycle_event === "npx" || /[\\/]_npx[\\/]/.test(io.scriptPath ?? "");
|
|
7695
7764
|
}
|
|
7696
7765
|
function setCommandName(io) {
|
|
7697
|
-
prefix = startedByNpx(io) ? "npx amdahl" : "amdahl";
|
|
7766
|
+
prefix = startedByNpx(io) ? "npx -y amdahl" : "amdahl";
|
|
7698
7767
|
}
|
|
7699
7768
|
function amdahlCommand(rest = "") {
|
|
7700
7769
|
return rest ? `${prefix} ${rest}` : prefix;
|
|
@@ -7848,7 +7917,7 @@ function parseCommandLine(argv) {
|
|
|
7848
7917
|
const ref = second && second.includes("/") ? second : "<publisher>/<skill>";
|
|
7849
7918
|
throw usageError(
|
|
7850
7919
|
second === void 0 ? "`amdahl skills` needs a subcommand." : `Unknown command \`amdahl skills ${second}\`.`,
|
|
7851
|
-
`To add a skill: npx amdahl skills add ${ref}`
|
|
7920
|
+
`To add a skill: npx -y amdahl skills add ${ref}`
|
|
7852
7921
|
);
|
|
7853
7922
|
}
|
|
7854
7923
|
if (second === void 0) throw usageError(`\`amdahl ${first}\` needs a subcommand.`);
|
|
@@ -7890,12 +7959,14 @@ function parseErrorMessage(err) {
|
|
|
7890
7959
|
var USAGE = `Usage: amdahl <command> [options]
|
|
7891
7960
|
|
|
7892
7961
|
What you can do:
|
|
7893
|
-
amdahl Set up Claude Code, Cursor or Codex
|
|
7894
|
-
|
|
7895
|
-
Run by an agent or with no
|
|
7896
|
-
sets up that agent, or every
|
|
7897
|
-
finds, and asks nothing (also:
|
|
7962
|
+
amdahl Set up Claude Code, Cursor or Codex, or try
|
|
7963
|
+
Amdahl in this terminal. In a terminal it
|
|
7964
|
+
asks which. Run by an agent or with no
|
|
7965
|
+
terminal, it sets up that agent, or every
|
|
7966
|
+
client it finds, and asks nothing (also:
|
|
7967
|
+
amdahl setup)
|
|
7898
7968
|
amdahl optimize draft.md Rewrite an outbound email or LinkedIn message
|
|
7969
|
+
(with no file, at a terminal, paste it)
|
|
7899
7970
|
amdahl status Check that this workspace can optimize
|
|
7900
7971
|
amdahl keys create --name ci Make an API key for a server or CI
|
|
7901
7972
|
amdahl install claude-code Connect one AI client (also codex, cursor)
|
|
@@ -8836,6 +8907,8 @@ async function browserSignIn(ctx) {
|
|
|
8836
8907
|
};
|
|
8837
8908
|
ctx.config.default_profile = name;
|
|
8838
8909
|
ctx.saveConfig();
|
|
8910
|
+
ctx.profileName = name;
|
|
8911
|
+
ctx.profile = ctx.config.profiles[name] ?? null;
|
|
8839
8912
|
return { profile: name, me, workspace: ws };
|
|
8840
8913
|
} finally {
|
|
8841
8914
|
stopKeys();
|
|
@@ -8845,6 +8918,10 @@ async function browserSignIn(ctx) {
|
|
|
8845
8918
|
function signedInLine(ctx, { profile, me, workspace }) {
|
|
8846
8919
|
return `Signed in as ${printable(me.email ?? me.user_id)} to ${ctx.out.bold(printable(workspace.name))} (${printable(workspace.slug)}). Profile: ${profile}.`;
|
|
8847
8920
|
}
|
|
8921
|
+
function loginNextLine() {
|
|
8922
|
+
return `Next: run \`${amdahlCommand("optimize")}\` and paste an email,
|
|
8923
|
+
or \`${amdahlCommand()}\` to add Amdahl to Claude Code, Cursor or Codex.`;
|
|
8924
|
+
}
|
|
8848
8925
|
async function login(ctx) {
|
|
8849
8926
|
const signedIn = await browserSignIn(ctx);
|
|
8850
8927
|
const { profile, me, workspace } = signedIn;
|
|
@@ -8857,7 +8934,8 @@ async function login(ctx) {
|
|
|
8857
8934
|
scopes: me.scopes,
|
|
8858
8935
|
api_url: ctx.apiUrl
|
|
8859
8936
|
},
|
|
8860
|
-
signedInLine(ctx, signedIn)
|
|
8937
|
+
`${signedInLine(ctx, signedIn)}
|
|
8938
|
+
${loginNextLine()}`
|
|
8861
8939
|
);
|
|
8862
8940
|
return EXIT.ok;
|
|
8863
8941
|
}
|
|
@@ -9043,9 +9121,14 @@ async function npmGlobalPrefix(ctx) {
|
|
|
9043
9121
|
return prefix2 || null;
|
|
9044
9122
|
}
|
|
9045
9123
|
async function claudeHasAmdahl(ctx, command, mcpUrl) {
|
|
9046
|
-
|
|
9047
|
-
|
|
9124
|
+
let answer = claudeAnswers.get(ctx);
|
|
9125
|
+
if (!answer) {
|
|
9126
|
+
answer = ctx.io.run(command, ["mcp", "get", "amdahl"]).then((res) => res.code === 0 && res.stdout.split("\n").some((line) => line.trim() === `URL: ${mcpUrl}`));
|
|
9127
|
+
claudeAnswers.set(ctx, answer);
|
|
9128
|
+
}
|
|
9129
|
+
return answer;
|
|
9048
9130
|
}
|
|
9131
|
+
var claudeAnswers = /* @__PURE__ */ new WeakMap();
|
|
9049
9132
|
var CONFIG_HINT = "Fix the file, or move it away, then run the same command again.";
|
|
9050
9133
|
function readJsonObject(path) {
|
|
9051
9134
|
if (!existsSync3(path)) return { config: {}, existed: false };
|
|
@@ -9138,10 +9221,7 @@ function updateConfig(path, fn) {
|
|
|
9138
9221
|
throw new CliError("write_failed", `Could not update ${path} (${reason}).`, EXIT.general);
|
|
9139
9222
|
}
|
|
9140
9223
|
}
|
|
9141
|
-
async function
|
|
9142
|
-
const skill = skillPath(client, ctx.io.homedir);
|
|
9143
|
-
const current = existsSync3(skill) ? readFileSync3(skill, "utf8") : null;
|
|
9144
|
-
if (current === null || current !== SKILL_TEXT && isShippedSkill(current)) return false;
|
|
9224
|
+
async function hasAmdahl(ctx, client) {
|
|
9145
9225
|
const mcpUrl = `${ctx.apiUrl}/mcp`;
|
|
9146
9226
|
if (client === "claude-code") {
|
|
9147
9227
|
if (claudeFileHasAmdahl(claudeConfigPath(ctx), mcpUrl)) return true;
|
|
@@ -9159,6 +9239,12 @@ async function isInstalled(ctx, client) {
|
|
|
9159
9239
|
return false;
|
|
9160
9240
|
}
|
|
9161
9241
|
}
|
|
9242
|
+
async function isInstalled(ctx, client) {
|
|
9243
|
+
const skill = skillPath(client, ctx.io.homedir);
|
|
9244
|
+
const current = existsSync3(skill) ? readFileSync3(skill, "utf8") : null;
|
|
9245
|
+
if (current === null || current !== SKILL_TEXT && isShippedSkill(current)) return false;
|
|
9246
|
+
return hasAmdahl(ctx, client);
|
|
9247
|
+
}
|
|
9162
9248
|
async function installClient(ctx, client, print) {
|
|
9163
9249
|
const mcp = await installMcp(ctx, client, print);
|
|
9164
9250
|
return { mcp, skill: installSkill(ctx, client, print) };
|
|
@@ -9211,6 +9297,7 @@ async function installMcp(ctx, client, print) {
|
|
|
9211
9297
|
if (claudeFileHasAmdahl(claudeConfigPath(ctx), mcpUrl) || await claudeHasAmdahl(ctx, route.command, mcpUrl)) {
|
|
9212
9298
|
return { data: { ok: true, client, action: "unchanged", command }, human: `Claude Code already has Amdahl at ${mcpUrl}; nothing changed.`, notes: [] };
|
|
9213
9299
|
}
|
|
9300
|
+
claudeAnswers.delete(ctx);
|
|
9214
9301
|
const res = await io.run(route.command, args);
|
|
9215
9302
|
if (res.code !== 0) {
|
|
9216
9303
|
throw new CliError("install_failed", `\`${command}\` failed: ${res.stderr.trim() || `exit ${res.code}`}`, EXIT.general);
|
|
@@ -9441,6 +9528,7 @@ async function keysRevoke(ctx, args) {
|
|
|
9441
9528
|
|
|
9442
9529
|
// src/commands/optimize.ts
|
|
9443
9530
|
import { readFileSync as readFileSync4 } from "fs";
|
|
9531
|
+
var NEW_USER = "New to Amdahl? The sign-in sets up your free workspace.";
|
|
9444
9532
|
var OPTIMIZE_TIMEOUT_MS = 18e4;
|
|
9445
9533
|
var CHANNELS = ["email", "linkedin"];
|
|
9446
9534
|
var EVIDENCE = ["off", "workspace"];
|
|
@@ -9585,10 +9673,86 @@ function whatAmdahlDid(result) {
|
|
|
9585
9673
|
const kept = new Set([...lines].sort((a, b) => a.rank - b.rank).slice(0, MAX_LINES));
|
|
9586
9674
|
return lines.filter((l) => kept.has(l)).map((l) => l.line);
|
|
9587
9675
|
}
|
|
9676
|
+
var PASTE_PROMPT = "Paste an email or LinkedIn message, then press Enter on an empty line:";
|
|
9677
|
+
var VERSION_RULE = "\u2500\u2500 Amdahl's version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
|
|
9678
|
+
function nextTimeLine(ctx) {
|
|
9679
|
+
const file = amdahlCommand("optimize draft.txt");
|
|
9680
|
+
return ctx.io.platform === "darwin" ? `Next time: ${file},
|
|
9681
|
+
or pbpaste | ${amdahlCommand("optimize -")}` : `Next time: ${file}`;
|
|
9682
|
+
}
|
|
9683
|
+
function requestOptions(ctx) {
|
|
9684
|
+
const { flags } = ctx;
|
|
9685
|
+
const base = {};
|
|
9686
|
+
const channel = oneOf("channel", str(flags.channel), CHANNELS);
|
|
9687
|
+
const evidence = oneOf("evidence", str(flags.evidence), EVIDENCE);
|
|
9688
|
+
const onUnsupported = oneOf("on-unsupported", str(flags["on-unsupported"]), ON_UNSUPPORTED);
|
|
9689
|
+
if (channel) base.channel = channel;
|
|
9690
|
+
if (evidence) base.evidence = evidence;
|
|
9691
|
+
if (onUnsupported) base.on_unsupported = onUnsupported;
|
|
9692
|
+
if (flags.tries === true) base.include_tries = true;
|
|
9693
|
+
const contextPath = str(flags.context);
|
|
9694
|
+
if (contextPath) base.context = readContext(contextPath);
|
|
9695
|
+
return base;
|
|
9696
|
+
}
|
|
9697
|
+
async function optimizeOne(ctx, credential, message2, base, file) {
|
|
9698
|
+
try {
|
|
9699
|
+
return await withTimeout(
|
|
9700
|
+
apiClient(ctx, credential).request("/api/platform/v1/messages/optimize", {
|
|
9701
|
+
method: "POST",
|
|
9702
|
+
body: { message: message2, ...base }
|
|
9703
|
+
}),
|
|
9704
|
+
OPTIMIZE_TIMEOUT_MS,
|
|
9705
|
+
file
|
|
9706
|
+
);
|
|
9707
|
+
} catch (err) {
|
|
9708
|
+
throw toCliError(err);
|
|
9709
|
+
}
|
|
9710
|
+
}
|
|
9711
|
+
async function credentialOrSignIn(ctx) {
|
|
9712
|
+
try {
|
|
9713
|
+
return await resolveCredential(ctx);
|
|
9714
|
+
} catch (err) {
|
|
9715
|
+
if (!(err instanceof CliError && err.exit === EXIT.unauthenticated)) throw err;
|
|
9716
|
+
}
|
|
9717
|
+
ctx.out.info(`Sign in first, then paste your message.
|
|
9718
|
+
${NEW_USER}`);
|
|
9719
|
+
const signedIn = await browserSignIn(ctx);
|
|
9720
|
+
ctx.out.info(`\u2713 Signed in \xB7 ${printable(signedIn.workspace.name)}`);
|
|
9721
|
+
return resolveCredential(ctx);
|
|
9722
|
+
}
|
|
9723
|
+
async function optimizePasted(ctx) {
|
|
9724
|
+
const { io, out } = ctx;
|
|
9725
|
+
const base = requestOptions(ctx);
|
|
9726
|
+
const credential = await credentialOrSignIn(ctx);
|
|
9727
|
+
const message2 = await io.readPaste(PASTE_PROMPT);
|
|
9728
|
+
if (!message2.trim()) {
|
|
9729
|
+
out.info(`Nothing pasted, so nothing was sent.
|
|
9730
|
+
${nextTimeLine(ctx)}`);
|
|
9731
|
+
return EXIT.ok;
|
|
9732
|
+
}
|
|
9733
|
+
out.info("Amdahl is rewriting it. This can take a minute.");
|
|
9734
|
+
const result = await optimizeOne(ctx, credential, message2, base, "your message");
|
|
9735
|
+
if (result.ok === false) {
|
|
9736
|
+
if (out.json) out.result({ ok: false, file: "-", result });
|
|
9737
|
+
out.info(`Not optimized (${String(result.reason ?? "unknown")}): ${String(result.detail ?? "")}`);
|
|
9738
|
+
return EXIT.general;
|
|
9739
|
+
}
|
|
9740
|
+
if (out.json) {
|
|
9741
|
+
out.result({ ok: true, file: "-", result });
|
|
9742
|
+
return EXIT.ok;
|
|
9743
|
+
}
|
|
9744
|
+
out.info(VERSION_RULE);
|
|
9745
|
+
if (result.unchanged === true) out.info("No rewrite beat your draft; it is returned unchanged.");
|
|
9746
|
+
out.result({}, String(result.message ?? ""));
|
|
9747
|
+
const did = whatAmdahlDid(result);
|
|
9748
|
+
out.info([...did, nextTimeLine(ctx)].join("\n"));
|
|
9749
|
+
return EXIT.ok;
|
|
9750
|
+
}
|
|
9588
9751
|
async function optimize(ctx, args) {
|
|
9589
|
-
const { io, out
|
|
9752
|
+
const { io, out } = ctx;
|
|
9590
9753
|
let files = args;
|
|
9591
9754
|
if (files.length === 0) {
|
|
9755
|
+
if (io.stdinIsTTY && !ctx.nonInteractive) return optimizePasted(ctx);
|
|
9592
9756
|
if (io.stdinIsTTY) {
|
|
9593
9757
|
throw usageError(
|
|
9594
9758
|
"Give a file to optimize, or pipe the message on stdin.",
|
|
@@ -9598,16 +9762,7 @@ async function optimize(ctx, args) {
|
|
|
9598
9762
|
files = ["-"];
|
|
9599
9763
|
}
|
|
9600
9764
|
if (files.filter((f) => f === "-").length > 1) throw usageError("Stdin (-) can be read only once.");
|
|
9601
|
-
const base =
|
|
9602
|
-
const channel = oneOf("channel", str(flags.channel), CHANNELS);
|
|
9603
|
-
const evidence = oneOf("evidence", str(flags.evidence), EVIDENCE);
|
|
9604
|
-
const onUnsupported = oneOf("on-unsupported", str(flags["on-unsupported"]), ON_UNSUPPORTED);
|
|
9605
|
-
if (channel) base.channel = channel;
|
|
9606
|
-
if (evidence) base.evidence = evidence;
|
|
9607
|
-
if (onUnsupported) base.on_unsupported = onUnsupported;
|
|
9608
|
-
if (flags.tries === true) base.include_tries = true;
|
|
9609
|
-
const contextPath = str(flags.context);
|
|
9610
|
-
if (contextPath) base.context = readContext(contextPath);
|
|
9765
|
+
const base = requestOptions(ctx);
|
|
9611
9766
|
const inputs = [];
|
|
9612
9767
|
for (const file of files) {
|
|
9613
9768
|
let message2;
|
|
@@ -9620,23 +9775,10 @@ async function optimize(ctx, args) {
|
|
|
9620
9775
|
inputs.push({ file, message: message2 });
|
|
9621
9776
|
}
|
|
9622
9777
|
const credential = await resolveCredential(ctx);
|
|
9623
|
-
const client = apiClient(ctx, credential);
|
|
9624
9778
|
const results = [];
|
|
9625
9779
|
let exit = EXIT.ok;
|
|
9626
9780
|
for (const { file, message: message2 } of inputs) {
|
|
9627
|
-
|
|
9628
|
-
try {
|
|
9629
|
-
result = await withTimeout(
|
|
9630
|
-
client.request("/api/platform/v1/messages/optimize", {
|
|
9631
|
-
method: "POST",
|
|
9632
|
-
body: { message: message2, ...base }
|
|
9633
|
-
}),
|
|
9634
|
-
OPTIMIZE_TIMEOUT_MS,
|
|
9635
|
-
file
|
|
9636
|
-
);
|
|
9637
|
-
} catch (err) {
|
|
9638
|
-
throw toCliError(err);
|
|
9639
|
-
}
|
|
9781
|
+
const result = await optimizeOne(ctx, credential, message2, base, file);
|
|
9640
9782
|
const ok = result.ok !== false;
|
|
9641
9783
|
results.push({ ok, file, result });
|
|
9642
9784
|
if (!ok) {
|
|
@@ -9942,10 +10084,83 @@ var SHOWN = [
|
|
|
9942
10084
|
{ client: "codex", name: "Codex" }
|
|
9943
10085
|
];
|
|
9944
10086
|
var QUESTION = "Where should Amdahl work?";
|
|
10087
|
+
var TERMINAL = "This terminal";
|
|
9945
10088
|
var FOUND_TAG = "found on your machine";
|
|
9946
|
-
var
|
|
9947
|
-
var
|
|
9948
|
-
|
|
10089
|
+
var CONNECT_URL = "https://amdahl.ai/connect";
|
|
10090
|
+
var NONE_FOUND = `We didn't find Claude Code, Cursor or Codex on this computer.
|
|
10091
|
+
Using the Claude or ChatGPT app? Connect it at ${CONNECT_URL}`;
|
|
10092
|
+
var MENU_HINT = "\u2191\u2193 move \xB7 Space tick/untick \xB7 Enter continue";
|
|
10093
|
+
var ASK = '"Use Amdahl to make this sound less like AI"';
|
|
10094
|
+
var SIGN_IN = {
|
|
10095
|
+
"Claude Code": "If it's open, quit and reopen it. Type /mcp, pick amdahl, sign in.",
|
|
10096
|
+
// Cursor reads ~/.cursor/mcp.json at start, and lists its MCP servers under Customize > MCPs.
|
|
10097
|
+
Cursor: "If it's open, quit and reopen it. In Customize > MCPs, sign in to amdahl."
|
|
10098
|
+
};
|
|
10099
|
+
var TRY_IN = {
|
|
10100
|
+
"Claude Code": `Paste an email and say: ${ASK}`,
|
|
10101
|
+
Cursor: `In chat, paste an email, say: ${ASK}`,
|
|
10102
|
+
Codex: `In codex, paste an email, say: ${ASK}`
|
|
10103
|
+
};
|
|
10104
|
+
var CODEX_TRY = `Run codex, paste an email and say: ${ASK}`;
|
|
10105
|
+
var CODEX_LOGIN = "codex mcp login amdahl";
|
|
10106
|
+
function toolScreen(name, already) {
|
|
10107
|
+
return [
|
|
10108
|
+
already ? `\u2713 ${name} already has Amdahl.` : `\u2713 Added Amdahl to ${name}.`,
|
|
10109
|
+
`One step left, in ${name}:`,
|
|
10110
|
+
` 1. ${SIGN_IN[name] ?? ""}`,
|
|
10111
|
+
` 2. ${TRY_IN[name] ?? ""}`
|
|
10112
|
+
];
|
|
10113
|
+
}
|
|
10114
|
+
function allSetLines(names) {
|
|
10115
|
+
const [only] = names;
|
|
10116
|
+
if (only === void 0) return [];
|
|
10117
|
+
const tryHere2 = `Want to try it here instead? Run: ${amdahlCommand("optimize")}`;
|
|
10118
|
+
if (names.length === 1) {
|
|
10119
|
+
const done = {
|
|
10120
|
+
"Claude Code": ["If you've already run /mcp, you're done.", "If not: quit and reopen Claude Code, type /mcp, pick amdahl, sign in."],
|
|
10121
|
+
Cursor: ["If you've already signed in under Customize > MCPs, you're done.", "If not: quit and reopen Cursor, then sign in to amdahl there."],
|
|
10122
|
+
Codex: [`If you've already run ${CODEX_LOGIN}, you're done.`, `If not, run it now in this terminal.`]
|
|
10123
|
+
};
|
|
10124
|
+
return [`Amdahl is in ${only}. You're all set.`, `Paste an email there and say: ${ASK}`, ...done[only] ?? [], tryHere2];
|
|
10125
|
+
}
|
|
10126
|
+
const step = {
|
|
10127
|
+
"Claude Code": "quit and reopen it, type /mcp, pick amdahl, sign in",
|
|
10128
|
+
Cursor: "quit and reopen it, then in Customize > MCPs, sign in to amdahl",
|
|
10129
|
+
Codex: `run ${CODEX_LOGIN}`
|
|
10130
|
+
};
|
|
10131
|
+
return [
|
|
10132
|
+
`Amdahl is in ${joinNames(names)}. You're all set.`,
|
|
10133
|
+
`Paste an email in ${names.length === 2 ? "either" : "any of them"} and say: ${ASK}`,
|
|
10134
|
+
"If you've already signed each one in, you're done. If not:",
|
|
10135
|
+
...names.map((n) => ` ${n}: ${step[n] ?? ""}`),
|
|
10136
|
+
tryHere2
|
|
10137
|
+
];
|
|
10138
|
+
}
|
|
10139
|
+
function nothingPickedLines() {
|
|
10140
|
+
return [
|
|
10141
|
+
"Nothing picked, so nothing changed.",
|
|
10142
|
+
`To try it here, run ${amdahlCommand()} again and tick "${TERMINAL}".`,
|
|
10143
|
+
`Using the Claude or ChatGPT app? Connect it at ${CONNECT_URL}`
|
|
10144
|
+
];
|
|
10145
|
+
}
|
|
10146
|
+
function wrapLine(line, columns) {
|
|
10147
|
+
if (!columns || [...line].length <= columns) return [line];
|
|
10148
|
+
const lead = /^\s*(?:\d+\.\s+)?/.exec(line)?.[0] ?? "";
|
|
10149
|
+
const indent = " ".repeat(lead.length);
|
|
10150
|
+
const lines = [];
|
|
10151
|
+
let current = lead;
|
|
10152
|
+
let words = 0;
|
|
10153
|
+
for (const word of line.slice(lead.length).split(" ")) {
|
|
10154
|
+
if (words > 0 && [...`${current} ${word}`].length > columns) {
|
|
10155
|
+
lines.push(current);
|
|
10156
|
+
current = indent + word;
|
|
10157
|
+
} else current = words > 0 ? `${current} ${word}` : current + word;
|
|
10158
|
+
words++;
|
|
10159
|
+
}
|
|
10160
|
+
lines.push(current);
|
|
10161
|
+
return lines;
|
|
10162
|
+
}
|
|
10163
|
+
var AGENT_ASK = `paste an email and say ${ASK}`;
|
|
9949
10164
|
function finishLines(names, inClaudeCode = false) {
|
|
9950
10165
|
const [only] = names;
|
|
9951
10166
|
if (only === void 0) return [];
|
|
@@ -9953,16 +10168,16 @@ function finishLines(names, inClaudeCode = false) {
|
|
|
9953
10168
|
const step2 = {
|
|
9954
10169
|
"Claude Code": inClaudeCode ? "Before that, restart Claude Code and run /mcp once to connect Amdahl." : "Before that, run /mcp once in Claude Code to connect Amdahl.",
|
|
9955
10170
|
Cursor: "Before that, sign in when Cursor asks, the first time you use Amdahl.",
|
|
9956
|
-
Codex:
|
|
10171
|
+
Codex: `Before that, run \`${CODEX_LOGIN}\` once to connect Amdahl.`
|
|
9957
10172
|
};
|
|
9958
|
-
return [`Try it: ${
|
|
10173
|
+
return [`Try it: ${AGENT_ASK} in ${only}.`, step2[only] ?? ""];
|
|
9959
10174
|
}
|
|
9960
10175
|
const step = {
|
|
9961
10176
|
"Claude Code": inClaudeCode ? "restart it, then run /mcp" : "run /mcp",
|
|
9962
10177
|
Cursor: "sign in when it asks, on first use",
|
|
9963
|
-
Codex:
|
|
10178
|
+
Codex: `run \`${CODEX_LOGIN}\``
|
|
9964
10179
|
};
|
|
9965
|
-
return [`Try it: ${
|
|
10180
|
+
return [`Try it: ${AGENT_ASK} in any of them.`, "Before that, connect each one once:", ...names.map((n) => ` ${n}: ${step[n] ?? ""}`)];
|
|
9966
10181
|
}
|
|
9967
10182
|
var isSet = (value) => Boolean(value) && value !== "false" && value !== "0";
|
|
9968
10183
|
function ciVariable(env) {
|
|
@@ -10008,35 +10223,70 @@ function reportError(io, err) {
|
|
|
10008
10223
|
${err.hint ? `${err.hint}
|
|
10009
10224
|
` : ""}`);
|
|
10010
10225
|
}
|
|
10011
|
-
async function installEach(ctx,
|
|
10226
|
+
async function installEach(ctx, tools) {
|
|
10012
10227
|
let exit = EXIT.ok;
|
|
10013
10228
|
const ready = [];
|
|
10014
|
-
for (const
|
|
10229
|
+
for (const tool of tools) {
|
|
10015
10230
|
let steps;
|
|
10016
10231
|
try {
|
|
10017
|
-
steps = await installClient(ctx, client, false);
|
|
10232
|
+
steps = await installClient(ctx, tool.client, false);
|
|
10018
10233
|
} catch (raw) {
|
|
10019
|
-
const err = raw instanceof CliError ? raw : new CliError("install_failed", `Could not set up ${name}: ${raw instanceof Error ? raw.message : String(raw)}`, EXIT.general);
|
|
10234
|
+
const err = raw instanceof CliError ? raw : new CliError("install_failed", `Could not set up ${tool.name}: ${raw instanceof Error ? raw.message : String(raw)}`, EXIT.general);
|
|
10020
10235
|
reportError(ctx.io, err);
|
|
10021
10236
|
if (exit === EXIT.ok) exit = err.exit;
|
|
10022
10237
|
continue;
|
|
10023
10238
|
}
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
ready.push(name);
|
|
10027
|
-
detail([steps.mcp.human, ...steps.mcp.notes, steps.skill.human, ...steps.skill.notes].filter(Boolean).join("\n"));
|
|
10239
|
+
const detail = [steps.mcp.human, ...steps.mcp.notes, steps.skill.human, ...steps.skill.notes].filter(Boolean).join("\n");
|
|
10240
|
+
ready.push({ ...tool, already: steps.mcp.data.action === "unchanged", detail });
|
|
10028
10241
|
}
|
|
10029
10242
|
return { ready, exit };
|
|
10030
10243
|
}
|
|
10244
|
+
function enterOrSkip(io) {
|
|
10245
|
+
return new Promise((resolve2) => {
|
|
10246
|
+
const stop = io.listenForEnter(
|
|
10247
|
+
() => {
|
|
10248
|
+
stop();
|
|
10249
|
+
resolve2(true);
|
|
10250
|
+
},
|
|
10251
|
+
() => resolve2(false)
|
|
10252
|
+
);
|
|
10253
|
+
});
|
|
10254
|
+
}
|
|
10255
|
+
async function codexScreen(ctx, tool, say, detail, lead) {
|
|
10256
|
+
say(tool.already ? "\u2713 Codex already has Amdahl." : "\u2713 Added Amdahl to Codex.");
|
|
10257
|
+
detail(tool.detail);
|
|
10258
|
+
for (const line of lead) say(line);
|
|
10259
|
+
if (!await onPath(ctx, "codex")) {
|
|
10260
|
+
say("One step left, in your terminal:");
|
|
10261
|
+
say(` 1. Run: ${CODEX_LOGIN}`);
|
|
10262
|
+
say(` 2. ${TRY_IN.Codex ?? ""}`);
|
|
10263
|
+
return;
|
|
10264
|
+
}
|
|
10265
|
+
say("Press Enter to sign Codex in to Amdahl (opens your browser).");
|
|
10266
|
+
if (!await enterOrSkip(ctx.io)) {
|
|
10267
|
+
say(`Skipped. To connect it later, run: ${CODEX_LOGIN}`);
|
|
10268
|
+
say(CODEX_TRY);
|
|
10269
|
+
return;
|
|
10270
|
+
}
|
|
10271
|
+
const code = await ctx.io.runInteractive("codex", ["mcp", "login", "amdahl"]);
|
|
10272
|
+
if (code === 0) say("\u2713 Codex is connected.");
|
|
10273
|
+
else {
|
|
10274
|
+
say("");
|
|
10275
|
+
say(`Codex didn't finish signing in. To try again, run: ${CODEX_LOGIN}`);
|
|
10276
|
+
}
|
|
10277
|
+
say(CODEX_TRY);
|
|
10278
|
+
}
|
|
10031
10279
|
async function setup(ctx, mode) {
|
|
10032
10280
|
const { io, out } = ctx;
|
|
10033
10281
|
const verbose = ctx.flags.verbose === true;
|
|
10034
|
-
const say = (line) =>
|
|
10282
|
+
const say = (line) => {
|
|
10283
|
+
for (const part of mode === "menu" ? wrapLine(line, io.columns()) : [line]) io.stdout(`${part}
|
|
10035
10284
|
`);
|
|
10285
|
+
};
|
|
10036
10286
|
const detail = (text2) => {
|
|
10037
|
-
if (verbose && text2)
|
|
10287
|
+
if (verbose && text2) io.stdout(`${text2.replace(/^/gm, " ")}
|
|
10288
|
+
`);
|
|
10038
10289
|
};
|
|
10039
|
-
const inClaudeCode = Boolean(io.env.CLAUDECODE);
|
|
10040
10290
|
say(`${out.bold("Amdahl")} \xB7 predictive ML for GTM`);
|
|
10041
10291
|
const found = await detectClients(ctx);
|
|
10042
10292
|
const detected = SHOWN.filter((c) => found[c.client]);
|
|
@@ -10049,43 +10299,41 @@ ${installLine()}
|
|
|
10049
10299
|
`);
|
|
10050
10300
|
return EXIT.general;
|
|
10051
10301
|
}
|
|
10052
|
-
const { ready, exit: exit2 } = await installEach(ctx, targets
|
|
10053
|
-
for (const
|
|
10302
|
+
const { ready, exit: exit2 } = await installEach(ctx, targets);
|
|
10303
|
+
for (const tool of ready) {
|
|
10304
|
+
say(tool.already ? `\u2713 ${tool.name} already has Amdahl` : `\u2713 Added to ${tool.name}`);
|
|
10305
|
+
detail(tool.detail);
|
|
10306
|
+
}
|
|
10307
|
+
for (const line of finishLines(ready.map((t) => t.name), Boolean(io.env.CLAUDECODE))) say(line);
|
|
10054
10308
|
return exit2;
|
|
10055
10309
|
}
|
|
10056
|
-
let who;
|
|
10057
10310
|
if (detected.length > 0) {
|
|
10058
|
-
const
|
|
10059
|
-
if (
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
if (unreachable || who?.kind === "in") {
|
|
10068
|
-
say(`You're set up in ${joinNames(detected.map((c) => c.name))}.${unreachable ? " (Could not reach Amdahl to check your sign-in.)" : ""}`);
|
|
10069
|
-
for (const line of finishLines(detected.map((c) => c.name), inClaudeCode)) say(line);
|
|
10070
|
-
return EXIT.ok;
|
|
10071
|
-
}
|
|
10311
|
+
const there = await Promise.all(detected.map((c) => hasAmdahl(ctx, c.client)));
|
|
10312
|
+
if (there.every(Boolean)) {
|
|
10313
|
+
for (const tool of detected) {
|
|
10314
|
+
if (await isInstalled(ctx, tool.client)) continue;
|
|
10315
|
+
const { ready } = await installEach(ctx, [tool]);
|
|
10316
|
+
for (const r of ready) detail(r.detail);
|
|
10317
|
+
}
|
|
10318
|
+
for (const line of allSetLines(detected.map((c) => c.name))) say(line);
|
|
10319
|
+
return EXIT.ok;
|
|
10072
10320
|
}
|
|
10073
10321
|
}
|
|
10074
|
-
const
|
|
10075
|
-
|
|
10076
|
-
SHOWN.map((c) => c.
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
);
|
|
10322
|
+
const rows = [...SHOWN.map((c) => c.name), TERMINAL];
|
|
10323
|
+
const picks = await io.multiSelect(QUESTION, rows, {
|
|
10324
|
+
ticked: [...SHOWN.map((c) => found[c.client]), false],
|
|
10325
|
+
tags: [...SHOWN.map((c) => found[c.client] ? FOUND_TAG : void 0), void 0],
|
|
10326
|
+
note: detected.length === 0 ? NONE_FOUND : void 0,
|
|
10327
|
+
// With no tool here, the marker starts on "This terminal": Space, then Enter.
|
|
10328
|
+
initial: detected.length === 0 ? SHOWN.length : void 0,
|
|
10329
|
+
hint: MENU_HINT,
|
|
10330
|
+
color: out.color
|
|
10331
|
+
});
|
|
10085
10332
|
if (picks.length === 0) {
|
|
10086
|
-
|
|
10333
|
+
for (const line of nothingPickedLines()) say(line);
|
|
10087
10334
|
return EXIT.ok;
|
|
10088
10335
|
}
|
|
10336
|
+
const here = picks.includes(SHOWN.length);
|
|
10089
10337
|
let exit = EXIT.ok;
|
|
10090
10338
|
const chosen = [];
|
|
10091
10339
|
for (const pick of picks.flatMap((i) => SHOWN[i] ?? [])) {
|
|
@@ -10097,22 +10345,51 @@ ${installLine()}
|
|
|
10097
10345
|
reportError(io, err);
|
|
10098
10346
|
if (exit === EXIT.ok) exit = err.exit;
|
|
10099
10347
|
}
|
|
10100
|
-
|
|
10101
|
-
|
|
10348
|
+
const done = await installEach(ctx, chosen);
|
|
10349
|
+
if (exit === EXIT.ok) exit = done.exit;
|
|
10350
|
+
let lead = [NEW_USER];
|
|
10351
|
+
for (const [i, tool] of done.ready.entries()) {
|
|
10352
|
+
if (i > 0) say("");
|
|
10353
|
+
if (tool.client === "codex") {
|
|
10354
|
+
await codexScreen(ctx, tool, say, detail, lead);
|
|
10355
|
+
} else {
|
|
10356
|
+
const [first, ...steps] = toolScreen(tool.name, tool.already);
|
|
10357
|
+
say(first ?? "");
|
|
10358
|
+
detail(tool.detail);
|
|
10359
|
+
for (const line of [...steps, ...lead]) say(line);
|
|
10360
|
+
}
|
|
10361
|
+
lead = [];
|
|
10362
|
+
}
|
|
10363
|
+
if (!here) return exit;
|
|
10364
|
+
if (done.ready.length > 0) say("");
|
|
10365
|
+
try {
|
|
10366
|
+
const code = await tryHere(ctx, say, lead);
|
|
10367
|
+
return exit === EXIT.ok ? code : exit;
|
|
10368
|
+
} catch (err) {
|
|
10369
|
+
if (err instanceof PromptCancelled && done.ready.length > 0) {
|
|
10370
|
+
throw new CliError("cancelled", `Stopped. Amdahl is still set up in ${joinNames(done.ready.map((t) => t.name))}.`, EXIT.cancelled);
|
|
10371
|
+
}
|
|
10372
|
+
throw err;
|
|
10373
|
+
}
|
|
10374
|
+
}
|
|
10375
|
+
async function tryHere(ctx, say, lead) {
|
|
10376
|
+
const { out } = ctx;
|
|
10377
|
+
const verbose = ctx.flags.verbose === true;
|
|
10378
|
+
const who = await signIn(ctx);
|
|
10102
10379
|
if (who.kind === "in") {
|
|
10103
10380
|
say(`\u2713 Signed in \xB7 ${who.workspace}`);
|
|
10104
|
-
|
|
10381
|
+
if (verbose) say(` Signed in as ${who.email}.`);
|
|
10105
10382
|
} else if (who.kind === "out") {
|
|
10383
|
+
for (const line of lead) say(line);
|
|
10106
10384
|
const signedIn = await browserSignIn(ctx);
|
|
10107
10385
|
say(`\u2713 Signed in \xB7 ${printable(signedIn.workspace.name)}`);
|
|
10108
|
-
|
|
10386
|
+
if (verbose) say(` ${signedInLine(ctx, signedIn)}`);
|
|
10109
10387
|
} else {
|
|
10110
10388
|
const name = who.source === "flag" ? "--api-key" : who.source.slice("env:".length);
|
|
10111
|
-
out.info(`Amdahl rejected the key in ${name}
|
|
10389
|
+
out.info(`Amdahl rejected the key in ${name}, so a message can't be tried here. Fix it, or unset it and run \`${amdahlCommand("login")}\`.`);
|
|
10390
|
+
return EXIT.unauthenticated;
|
|
10112
10391
|
}
|
|
10113
|
-
|
|
10114
|
-
for (const line of finishLines(done.ready, inClaudeCode)) say(line);
|
|
10115
|
-
return exit === EXIT.ok ? done.exit : exit;
|
|
10392
|
+
return optimizePasted(ctx);
|
|
10116
10393
|
}
|
|
10117
10394
|
|
|
10118
10395
|
// src/commands/skills.ts
|
|
@@ -10726,7 +11003,7 @@ async function workspaceUse(ctx, args) {
|
|
|
10726
11003
|
}
|
|
10727
11004
|
|
|
10728
11005
|
// src/version.ts
|
|
10729
|
-
var VERSION = "0.3.
|
|
11006
|
+
var VERSION = "0.3.5";
|
|
10730
11007
|
|
|
10731
11008
|
// src/main.ts
|
|
10732
11009
|
var COMMANDS = {
|
package/package.json
CHANGED