@amdahl/cli 0.4.0 → 0.4.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 +91 -30
- package/bin/amdahl.js +3 -3
- package/dist/amdahl.js +477 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,9 +44,8 @@ Full steps: https://amdahl.ai/connect
|
|
|
44
44
|
## Install
|
|
45
45
|
|
|
46
46
|
Needs Node.js 22 or later. Node.js 20 reached end of life in April 2026 and
|
|
47
|
-
gets no security fixes.
|
|
48
|
-
|
|
49
|
-
names the version it found, and links the Node.js download page.
|
|
47
|
+
gets no security fixes. On a Node older than 22, `amdahl` stops before it does
|
|
48
|
+
anything, names the version it found, and links the Node.js download page.
|
|
50
49
|
|
|
51
50
|
```sh
|
|
52
51
|
# Run once, with no install
|
|
@@ -372,7 +371,7 @@ amdahl open [--no-browser]
|
|
|
372
371
|
amdahl feedback <run-id> --evidence did_it_myself|reported_to_me|inferred
|
|
373
372
|
[--outcome used|used_with_edits|not_used|unsure] [--sent-text <file>|-] [--comment <text>]
|
|
374
373
|
amdahl sent <run-id> --provider <provider> --channel <channel> --sent-as returned|edited
|
|
375
|
-
|
|
374
|
+
[--provider-send-id <id>] [--recipient <email|url> | --recipient-key-hash <sha256>]
|
|
376
375
|
[--sent-at <time>] [--provider-campaign-id <id>] [--sequence-step <n>]
|
|
377
376
|
[--provider-variant-id <id>] [--provider-lead-id <id>] [--thread-id <id>]
|
|
378
377
|
[--sent-text <file>|-]
|
|
@@ -399,9 +398,20 @@ amdahl search "What objections came up in lost deals last quarter?"
|
|
|
399
398
|
(`POST /search/query`, with `answer: true`). It prints a short answer, with
|
|
400
399
|
each citation said in plain words, and the rows it came from under `From:`,
|
|
401
400
|
at most five. With no written answer it prints up to ten rows. Each row is
|
|
402
|
-
one line (its title, who and when) and up to three lines of its
|
|
403
|
-
columns. `--limit <n>` asks for
|
|
404
|
-
server's whole answer, every row
|
|
401
|
+
one line (its title, who and when) and up to three lines of its quote (the
|
|
402
|
+
row's `content_preview`, or its text), in 80 columns. `--limit <n>` asks for
|
|
403
|
+
at most `n` rows, and `--json` prints the server's whole answer, every row
|
|
404
|
+
included.
|
|
405
|
+
|
|
406
|
+
When the server found rows but wrote no answer over them (`not_enough_data`),
|
|
407
|
+
the CLI says so plainly before the rows: "Amdahl couldn't answer that from
|
|
408
|
+
these rows. Try rephrasing the question, or run `amdahl connections` to see
|
|
409
|
+
whether your data is synced." The rows follow under `Rows it read:`. A
|
|
410
|
+
failed answer says it found rows but could not write an answer.
|
|
411
|
+
|
|
412
|
+
The CLI sends the question as you typed it. A window such as "last month" is
|
|
413
|
+
read by the server from the words (the previous calendar month, in UTC); the
|
|
414
|
+
request has no separate field for a time window.
|
|
405
415
|
|
|
406
416
|
Research is something search starts, never a command of its own. Search
|
|
407
417
|
hands a statistical or long question to a research job by itself, and
|
|
@@ -422,11 +432,11 @@ theme blocks print as plain text (an `<unverified>` figure is marked
|
|
|
422
432
|
type this version does not know: both point to `--json`, where the whole block
|
|
423
433
|
is. Starting research needs
|
|
424
434
|
`conversations:write` and reading it back `conversations:read`. A CLI
|
|
425
|
-
sign-in has neither, so the server runs the search instead
|
|
426
|
-
|
|
435
|
+
sign-in has neither, so the server runs the search instead. The CLI says so
|
|
436
|
+
first, before any row:
|
|
427
437
|
|
|
428
438
|
```
|
|
429
|
-
|
|
439
|
+
Research needs a Customer agent key: amdahl keys create --name research --preset agent. Here is the search answer instead.
|
|
430
440
|
```
|
|
431
441
|
|
|
432
442
|
A workspace with nothing connected answers with `setup_needed`: the CLI
|
|
@@ -459,7 +469,10 @@ console's add page,
|
|
|
459
469
|
`<console>/<workspace>/settings/connections?add=1&connector=hubspot`. The
|
|
460
470
|
`connector` only fills the search box on that page: you pick HubSpot there and
|
|
461
471
|
sign in to it. With no connector it opens the add page with an empty search
|
|
462
|
-
box.
|
|
472
|
+
box. If that connector is already connected and healthy, the CLI says so
|
|
473
|
+
first ("HubSpot is already connected and healthy (last sync 2026-11-11).")
|
|
474
|
+
and prints the add page instead of opening it; one that needs attention gets
|
|
475
|
+
its own console page. The CLI never creates a connection itself. An unknown connector exits 2
|
|
463
476
|
and lists the ones you can connect; one that is not offered yet exits 1 and
|
|
464
477
|
says it is coming soon. It opens the browser only for a person at a terminal:
|
|
465
478
|
stdin and stdout are terminals, `AMDAHL_NO_PROMPT=1` is not set, no agent runs
|
|
@@ -494,18 +507,23 @@ The list is a CSV file with a header row, or a JSONL file (`.jsonl` or
|
|
|
494
507
|
`.ndjson`) with one batch item, a JSON object, per line. A CSV has a
|
|
495
508
|
`message` column and may have `item_id`, `channel`, `evidence`,
|
|
496
509
|
`on_unsupported`, `facts` and `rules`; facts and rules take one per line
|
|
497
|
-
inside the cell. Any other column
|
|
498
|
-
|
|
499
|
-
|
|
510
|
+
inside the cell. Any other column (a CRM export has many) is not sent: one
|
|
511
|
+
line names it, and it is copied to the results file in its place. Only a
|
|
512
|
+
missing `message` column, or a column Amdahl reads that appears twice (a
|
|
513
|
+
doubled `message`, say), is refused. A JSONL item takes any field a single
|
|
514
|
+
optimize takes, and the server refuses a key it does not know. The optimize flags (`--channel`,
|
|
500
515
|
`--evidence`, `--on-unsupported`, `--tries`, `--context`) apply to every
|
|
501
516
|
item that does not set its own; a row's facts and a `--context` file's rules
|
|
502
517
|
and voice examples are both sent.
|
|
503
518
|
|
|
504
|
-
The whole file is read and checked first. A
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
to the list: `drafts.csv`
|
|
508
|
-
|
|
519
|
+
The whole file is read and checked first. A missing `message` column says
|
|
520
|
+
which columns it reads. Progress goes to stderr, in place at a terminal: a
|
|
521
|
+
queued batch says it is queued, and each line after that says where it stands
|
|
522
|
+
once. When the batch completes, the results go next to the list: `drafts.csv`
|
|
523
|
+
gets `drafts.optimized.csv` (every input column in its original order, then
|
|
524
|
+
`optimized_message`, `status`, `summary`, `run_id` and `error`; a column of
|
|
525
|
+
yours with one of those names keeps it, and Amdahl's takes `amdahl_` in
|
|
526
|
+
front), and
|
|
509
527
|
`x.jsonl` gets `x.optimized.jsonl` (one batch item per line, its `result`
|
|
510
528
|
what a single optimize returns). With `--json`, the batch and every item go
|
|
511
529
|
to stdout and no file is written. A failed item exits 1. A refused list
|
|
@@ -513,7 +531,10 @@ names the line of the first item the server refused.
|
|
|
513
531
|
|
|
514
532
|
A spreadsheet runs a cell that starts with `=`, `+`, `-` or `@` as a formula,
|
|
515
533
|
so in the results CSV such a cell (and one that starts with a tab or a
|
|
516
|
-
carriage return) starts with a `'`.
|
|
534
|
+
carriage return) starts with a `'`. That includes the cells copied through
|
|
535
|
+
from your own columns: a phone number such as `+1 555 0100` or a negative
|
|
536
|
+
number gets the `'` too, and you delete it to use the value. The `'` is the
|
|
537
|
+
guard, so it stays. The CLI does not replace a results file
|
|
517
538
|
that already exists: it exits 2 before it queues anything, and `--force`
|
|
518
539
|
replaces the file.
|
|
519
540
|
|
|
@@ -527,7 +548,7 @@ here. `--no-wait` with `--batch` exits 2: it only queues a new list.
|
|
|
527
548
|
|
|
528
549
|
```sh
|
|
529
550
|
amdahl feedback <run-id> --evidence did_it_myself --outcome used_with_edits --sent-text sent.txt
|
|
530
|
-
amdahl sent <run-id> --provider gmail --channel email --sent-as edited --provider-send-id "<msg-id@mail.gmail.com>"
|
|
551
|
+
amdahl sent <run-id> --provider gmail --channel email --sent-as edited --sent-text sent.txt --provider-send-id "<msg-id@mail.gmail.com>"
|
|
531
552
|
```
|
|
532
553
|
|
|
533
554
|
`feedback` says what happened to the message one optimize run returned
|
|
@@ -543,16 +564,23 @@ stores two.
|
|
|
543
564
|
(`POST /messages/report_send`). A report from a CLI sign-in is stored as
|
|
544
565
|
`reported_via: cli`. The flags are the report's fields: `--provider`
|
|
545
566
|
(`lemlist`, `instantly`, `outreach`, `salesloft`, `hubspot`, `salesforce`,
|
|
546
|
-
`gmail`, `heyreach`, `apollo`, `linkedin_manual`, `other
|
|
547
|
-
|
|
567
|
+
`gmail`, `heyreach`, `apollo`, `linkedin_manual`, `other`; the server's
|
|
568
|
+
list, so use `other` for a sender that is not on it), `--channel` (`email`,
|
|
569
|
+
`linkedin_invite`, `linkedin_message`, `linkedin_inmail`; `linkedin` is
|
|
570
|
+
accepted and means `linkedin_message`, as in `amdahl optimize --channel`),
|
|
548
571
|
`--sent-as` (`returned` or `edited`) and `--sent-at` (a time with a time
|
|
549
|
-
zone; the current time when left out), then the optional ids.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
572
|
+
zone; the current time when left out), then the optional ids. `--sent-as
|
|
573
|
+
edited` needs `--sent-text`, the message as you sent it (Amdahl keeps only a
|
|
574
|
+
hash of it), and the file must not be empty. That is the CLI's rule, not the
|
|
575
|
+
server's. A run id given twice, or two run ids, exits 2.
|
|
576
|
+
|
|
577
|
+
The server stores a report only when it can tell which send it is, so you give
|
|
578
|
+
at least one of `--provider-send-id` and a recipient: `--recipient` (an email
|
|
579
|
+
address, or a LinkedIn profile URL for a LinkedIn channel) or
|
|
580
|
+
`--recipient-key-hash` (a SHA-256), never both. A send id with a recipient is
|
|
581
|
+
fine, and the server then counts a report with and one without the id as one
|
|
582
|
+
send. A campaign, a lead and a step are not enough. Without a send id or a
|
|
583
|
+
recipient, the CLI exits 2 before it sends anything. The id is the provider's own: for gmail, the
|
|
556
584
|
`Message-ID` header with its angle brackets; never an email address (give that
|
|
557
585
|
as `--recipient`; Amdahl stores only a hash of it). The command reports one
|
|
558
586
|
send, so give one run id. To report many at once, call
|
|
@@ -580,6 +608,39 @@ lines; `--json` never carries them.
|
|
|
580
608
|
a connection that needs attention, its console page; a used-up quota, the
|
|
581
609
|
day it resets; a role too low, ask a workspace admin.
|
|
582
610
|
- Bare `amdahl`, signed in, with every tool set up: what to try here.
|
|
611
|
+
- After an optimize (one file, a pasted message, or a list) a line says what
|
|
612
|
+
to do once the message is sent: two lines, each a command to paste, with
|
|
613
|
+
the run's own id and the flags it needs as placeholders (`amdahl sent
|
|
614
|
+
<run-id> --provider <provider> --channel <channel> --sent-as returned
|
|
615
|
+
--provider-send-id <id>`, then `amdahl feedback <run-id> --evidence
|
|
616
|
+
did_it_myself --outcome used`). A list prints the first run id of its
|
|
617
|
+
results. Replace each `<placeholder>` before you run it.
|
|
618
|
+
- `amdahl optimize` no longer prints the server's `what_changed` line. It is
|
|
619
|
+
the same sentence on every run. It is still in `--json`.
|
|
620
|
+
|
|
621
|
+
## Help and errors
|
|
622
|
+
|
|
623
|
+
`amdahl <command> --help` (also `-h`, or `amdahl help <command>`) prints that
|
|
624
|
+
command's usage and nothing else: its flags, what it prints, and its exit
|
|
625
|
+
codes where they matter. `amdahl keys --help` lists the `keys` commands. For
|
|
626
|
+
`optimize --json`, the help says where the message is: `.result.message`, and
|
|
627
|
+
the run id is `.result.run_id`.
|
|
628
|
+
|
|
629
|
+
Three failures print a plain sentence each. Only the sentence changed from
|
|
630
|
+
0.4.0: the `--json` error `code` is still the server's (or `http_<status>`) and
|
|
631
|
+
its `hint` is the same, because scripts and agents branch on them.
|
|
632
|
+
|
|
633
|
+
- A credential that lacks a scope says what it cannot do and what to use: `This
|
|
634
|
+
credential cannot optimize. Use a Customer agent key, or run \`amdahl
|
|
635
|
+
login\` again.` (exit 4). It never prints the server's `Missing required
|
|
636
|
+
scope(s)`.
|
|
637
|
+
- A key the server will not take (revoked or expired): `This key isn't valid
|
|
638
|
+
(revoked or expired).` and the hint, `Run amdahl login, or check
|
|
639
|
+
AMDAHL_KEY.` (exit 3). An OAuth access token in `AMDAHL_ACCESS_TOKEN` says
|
|
640
|
+
`This token isn't valid (revoked or expired).` A saved sign-in the server
|
|
641
|
+
will not take: `Your sign-in is no longer valid.`
|
|
642
|
+
- A file that is not there: `Can't find <file>.` (exit 2), for a draft, a
|
|
643
|
+
list, `--context` and `--sent-text`.
|
|
583
644
|
|
|
584
645
|
## Skills
|
|
585
646
|
|
package/bin/amdahl.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// bin/amdahl.js
|
|
3
3
|
//
|
|
4
4
|
// The `amdahl` command. It checks the Node.js version before it loads the
|
|
5
|
-
// CLI: dist/amdahl.js is built for Node
|
|
5
|
+
// CLI: dist/amdahl.js is built for Node 22, and an older Node fails to parse
|
|
6
6
|
// it or crashes in it, while npx only warns about `engines`. So this file
|
|
7
7
|
// uses no syntax newer than Node 14 (no `??=`, no top-level await, no class
|
|
8
8
|
// fields), and an old Node can run it far enough to say what to do.
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
var version = process.versions.node
|
|
11
11
|
var major = parseInt(version.split('.')[0], 10)
|
|
12
12
|
|
|
13
|
-
if (!(major >=
|
|
13
|
+
if (!(major >= 22)) {
|
|
14
14
|
process.stderr.write(
|
|
15
|
-
'amdahl needs Node.js
|
|
15
|
+
'amdahl needs Node.js 22 or later. This computer has Node.js ' +
|
|
16
16
|
version +
|
|
17
17
|
'.\nInstall a newer one from https://nodejs.org/en/download, then run the same command again.\n'
|
|
18
18
|
)
|
package/dist/amdahl.js
CHANGED
|
@@ -7921,6 +7921,13 @@ var ALIASES = {
|
|
|
7921
7921
|
"auth status": "whoami"
|
|
7922
7922
|
};
|
|
7923
7923
|
var TWO_WORD = /* @__PURE__ */ new Set(["auth", "keys", "workspace", "skills"]);
|
|
7924
|
+
function helpWords(argv) {
|
|
7925
|
+
try {
|
|
7926
|
+
return parseArgs({ args: argv, options: allOptions(), allowPositionals: true, strict: false }).positionals;
|
|
7927
|
+
} catch {
|
|
7928
|
+
return [];
|
|
7929
|
+
}
|
|
7930
|
+
}
|
|
7924
7931
|
function allOptions() {
|
|
7925
7932
|
const merged = { ...GLOBAL_OPTIONS };
|
|
7926
7933
|
for (const spec of Object.values(COMMAND_OPTIONS)) Object.assign(merged, spec);
|
|
@@ -8022,12 +8029,16 @@ Commands:
|
|
|
8022
8029
|
optimize [<file>... | -] [--channel email|linkedin]
|
|
8023
8030
|
[--evidence off|workspace] [--on-unsupported flag|remove]
|
|
8024
8031
|
[--tries] [--context <file.json>]
|
|
8032
|
+
--json prints {ok, file, result}: the message is .result.message
|
|
8033
|
+
and the run id is .result.run_id.
|
|
8025
8034
|
optimize --list <file.csv|file.jsonl> [--no-wait] [--batch <id>] [--force]
|
|
8026
8035
|
[the optimize flags above, for every message]
|
|
8027
|
-
A CSV
|
|
8028
|
-
evidence, on_unsupported, facts and rules.
|
|
8029
|
-
|
|
8030
|
-
|
|
8036
|
+
A CSV needs a message column, and may have item_id, channel,
|
|
8037
|
+
evidence, on_unsupported, facts and rules. Any other column is
|
|
8038
|
+
copied to the results as it is. --batch <id> picks up a batch
|
|
8039
|
+
queued earlier, with the same list. --force replaces an existing
|
|
8040
|
+
results file. In the results CSV, a cell that starts with =, +, -
|
|
8041
|
+
or @ (a phone number, say) gets a ' in front: it is no formula.
|
|
8031
8042
|
search "<question>" [--research] [--limit <n>]
|
|
8032
8043
|
--research ask for a research job: a deeper, cited answer (needs
|
|
8033
8044
|
a Customer agent key, amdahl keys create --preset
|
|
@@ -8041,12 +8052,19 @@ Commands:
|
|
|
8041
8052
|
What happened to a message
|
|
8042
8053
|
sent <run-id> --provider <provider> --channel <channel>
|
|
8043
8054
|
--sent-as returned|edited [--sent-at <time>]
|
|
8044
|
-
|
|
8045
|
-
|
|
8055
|
+
[--provider-send-id <id>]
|
|
8056
|
+
[--recipient <email|url> | --recipient-key-hash <sha256>]
|
|
8046
8057
|
[--provider-campaign-id <id>] [--sequence-step <n>]
|
|
8047
8058
|
[--provider-variant-id <id>] [--provider-lead-id <id>]
|
|
8048
8059
|
[--thread-id <id>] [--sent-text <file> | -]
|
|
8049
|
-
One send per command.
|
|
8060
|
+
One send per command. Give --provider-send-id, a recipient, or
|
|
8061
|
+
both (a recipient is --recipient or --recipient-key-hash, not both).
|
|
8062
|
+
--provider: lemlist, instantly, outreach, salesloft, hubspot,
|
|
8063
|
+
salesforce, gmail, heyreach, apollo, linkedin_manual,
|
|
8064
|
+
other (any sender not listed)
|
|
8065
|
+
--channel: email, linkedin (= linkedin_message), linkedin_invite,
|
|
8066
|
+
linkedin_message, linkedin_inmail
|
|
8067
|
+
--sent-as edited needs --sent-text.
|
|
8050
8068
|
Report that a message went out
|
|
8051
8069
|
keys create --name <name> [--preset read-only|agent|internal|admin]
|
|
8052
8070
|
[--expires 30d|90d|365d] [--no-browser]
|
|
@@ -8068,6 +8086,7 @@ Commands:
|
|
|
8068
8086
|
(Cancel or Install anyway); -y, --json or no terminal
|
|
8069
8087
|
stops (exit 9)
|
|
8070
8088
|
help Show this help
|
|
8089
|
+
Run \`amdahl <command> --help\` for one command's usage.
|
|
8071
8090
|
|
|
8072
8091
|
Global options:
|
|
8073
8092
|
--profile <name> Use a saved profile (default: AMDAHL_PROFILE, then the
|
|
@@ -8085,7 +8104,7 @@ Global options:
|
|
|
8085
8104
|
`;
|
|
8086
8105
|
|
|
8087
8106
|
// src/version.ts
|
|
8088
|
-
var VERSION = "0.4.
|
|
8107
|
+
var VERSION = "0.4.1";
|
|
8089
8108
|
|
|
8090
8109
|
// src/api.ts
|
|
8091
8110
|
var CLIENT_HEADER = {
|
|
@@ -8093,18 +8112,20 @@ var CLIENT_HEADER = {
|
|
|
8093
8112
|
"User-Agent": `amdahl-cli/${VERSION}`
|
|
8094
8113
|
};
|
|
8095
8114
|
var AmdahlApiError = class extends Error {
|
|
8096
|
-
constructor(message2, statusCode, path, responseBody, errorCode) {
|
|
8115
|
+
constructor(message2, statusCode, path, responseBody, errorCode, credentialSource) {
|
|
8097
8116
|
super(message2);
|
|
8098
8117
|
this.statusCode = statusCode;
|
|
8099
8118
|
this.path = path;
|
|
8100
8119
|
this.responseBody = responseBody;
|
|
8101
8120
|
this.errorCode = errorCode;
|
|
8121
|
+
this.credentialSource = credentialSource;
|
|
8102
8122
|
this.name = "AmdahlApiError";
|
|
8103
8123
|
}
|
|
8104
8124
|
statusCode;
|
|
8105
8125
|
path;
|
|
8106
8126
|
responseBody;
|
|
8107
8127
|
errorCode;
|
|
8128
|
+
credentialSource;
|
|
8108
8129
|
};
|
|
8109
8130
|
function extractToolFailure(json) {
|
|
8110
8131
|
if (!json || typeof json !== "object" || Array.isArray(json)) return null;
|
|
@@ -8176,7 +8197,7 @@ function assertUuid(name, value) {
|
|
|
8176
8197
|
}
|
|
8177
8198
|
}
|
|
8178
8199
|
function createAmdahlClient(config) {
|
|
8179
|
-
const { baseUrl, token } = config;
|
|
8200
|
+
const { baseUrl, token, source } = config;
|
|
8180
8201
|
async function request(path, options = {}) {
|
|
8181
8202
|
const { method = "GET", body, headers = {} } = options;
|
|
8182
8203
|
const allHeaders = { ...headers, ...CLIENT_HEADER, Authorization: `Bearer ${token}` };
|
|
@@ -8189,12 +8210,12 @@ function createAmdahlClient(config) {
|
|
|
8189
8210
|
if (!response.ok) {
|
|
8190
8211
|
const errorBody = await response.text().catch(() => "");
|
|
8191
8212
|
const { message: message2, code } = parseErrorBody(errorBody, response.status);
|
|
8192
|
-
throw new AmdahlApiError(message2, response.status, path, errorBody, code);
|
|
8213
|
+
throw new AmdahlApiError(message2, response.status, path, errorBody, code, source);
|
|
8193
8214
|
}
|
|
8194
8215
|
const json = await response.json();
|
|
8195
8216
|
const toolFailure = extractToolFailure(json);
|
|
8196
8217
|
if (toolFailure) {
|
|
8197
|
-
throw new AmdahlApiError(toolFailure.message, response.status, path, JSON.stringify(json), toolFailure.code);
|
|
8218
|
+
throw new AmdahlApiError(toolFailure.message, response.status, path, JSON.stringify(json), toolFailure.code, source);
|
|
8198
8219
|
}
|
|
8199
8220
|
if (json && typeof json === "object" && !Array.isArray(json) && "data" in json && Object.keys(json).length === 1) {
|
|
8200
8221
|
return json.data;
|
|
@@ -8804,7 +8825,7 @@ async function profileCredential(ctx) {
|
|
|
8804
8825
|
return { source: "profile", token: refreshed.access_token, profile: profileName };
|
|
8805
8826
|
}
|
|
8806
8827
|
function apiClient(ctx, credential) {
|
|
8807
|
-
return createAmdahlClient({ baseUrl: ctx.apiUrl, token: credential.token });
|
|
8828
|
+
return createAmdahlClient({ baseUrl: ctx.apiUrl, token: credential.token, source: credential.source });
|
|
8808
8829
|
}
|
|
8809
8830
|
function errorDetails(body) {
|
|
8810
8831
|
if (!body) return {};
|
|
@@ -8816,6 +8837,28 @@ function errorDetails(body) {
|
|
|
8816
8837
|
return {};
|
|
8817
8838
|
}
|
|
8818
8839
|
}
|
|
8840
|
+
var CALL_VERBS = [
|
|
8841
|
+
["/messages/optimize/batch", "optimize a list"],
|
|
8842
|
+
["/messages/optimize/runs/", "record feedback"],
|
|
8843
|
+
["/messages/optimize", "optimize"],
|
|
8844
|
+
["/messages/report_send", "report sends"],
|
|
8845
|
+
["/research/", "read research"],
|
|
8846
|
+
["/search/query", "search"],
|
|
8847
|
+
["/connections", "read connections"],
|
|
8848
|
+
["/setup/status", "read the setup status"]
|
|
8849
|
+
];
|
|
8850
|
+
function plainMessage(err) {
|
|
8851
|
+
if (err.statusCode === 401 && err.credentialSource) {
|
|
8852
|
+
if (err.credentialSource === "profile") return "Your sign-in is no longer valid.";
|
|
8853
|
+
const noun = err.credentialSource === "env:AMDAHL_ACCESS_TOKEN" ? "token" : "key";
|
|
8854
|
+
return `This ${noun} isn't valid (revoked or expired).`;
|
|
8855
|
+
}
|
|
8856
|
+
if (err.errorCode === "missing_scope" || /^Missing required scope/i.test(err.message)) {
|
|
8857
|
+
const verb = CALL_VERBS.find(([prefix2]) => err.path.includes(prefix2))?.[1] ?? "do this";
|
|
8858
|
+
return `This credential cannot ${verb}. Use a Customer agent key, or run \`${amdahlCommand("login")}\` again.`;
|
|
8859
|
+
}
|
|
8860
|
+
return null;
|
|
8861
|
+
}
|
|
8819
8862
|
function toCliError(err) {
|
|
8820
8863
|
if (err instanceof CliError) return err;
|
|
8821
8864
|
if (err instanceof PromptCancelled) return new CliError("cancelled", "Stopped. Nothing changed.", EXIT.cancelled);
|
|
@@ -8824,7 +8867,7 @@ function toCliError(err) {
|
|
|
8824
8867
|
const details = errorDetails(err.responseBody);
|
|
8825
8868
|
const retry = details.retry_after_seconds;
|
|
8826
8869
|
const hint = exit === EXIT.unauthenticated ? `Run \`${amdahlCommand("login")}\`, or check AMDAHL_KEY.` : typeof retry === "number" ? `Try again in ${retry} seconds.` : void 0;
|
|
8827
|
-
return new CliError(err.errorCode ?? `http_${err.statusCode}`, err.message, exit, hint);
|
|
8870
|
+
return new CliError(err.errorCode ?? `http_${err.statusCode}`, plainMessage(err) ?? err.message, exit, hint);
|
|
8828
8871
|
}
|
|
8829
8872
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
8830
8873
|
return new CliError("network_error", `Could not reach the Amdahl API: ${message2}`, EXIT.network, NETWORK_HINT);
|
|
@@ -9436,6 +9479,14 @@ function day(iso) {
|
|
|
9436
9479
|
const ms = Date.parse(iso);
|
|
9437
9480
|
return Number.isFinite(ms) ? new Date(ms).toISOString().slice(0, 10) : "";
|
|
9438
9481
|
}
|
|
9482
|
+
function hasFactsOrRules(context) {
|
|
9483
|
+
const c = record(context);
|
|
9484
|
+
const some = (list2) => Array.isArray(list2) && list2.some((entry) => typeof entry === "string" ? entry.trim() !== "" : !!record(entry));
|
|
9485
|
+
return !!c && (some(c.facts) || some(c.rules));
|
|
9486
|
+
}
|
|
9487
|
+
function truthfulSummary(summary, context) {
|
|
9488
|
+
return /facts and rules/i.test(summary) && !hasFactsOrRules(context) ? "" : summary;
|
|
9489
|
+
}
|
|
9439
9490
|
function count(n, one, many = `${one}s`) {
|
|
9440
9491
|
return `${n} ${n === 1 ? one : many}`;
|
|
9441
9492
|
}
|
|
@@ -9536,7 +9587,10 @@ async function connect(ctx, args) {
|
|
|
9536
9587
|
}
|
|
9537
9588
|
}
|
|
9538
9589
|
const slug = await workspaceSlug(ctx, credential);
|
|
9539
|
-
const
|
|
9590
|
+
const consoleUrl = consoleUrlFor(ctx.io, ctx.apiUrl);
|
|
9591
|
+
const url = connectUrl(consoleUrl, slug, entry?.type);
|
|
9592
|
+
const existing = entry ? await existingConnection(client, entry.type) : null;
|
|
9593
|
+
if (entry && existing) return alreadyConnected(ctx, entry, existing, url, connectionUrl(consoleUrl, slug, field(existing.id)));
|
|
9540
9594
|
const opened = await openOrPrint(ctx, url);
|
|
9541
9595
|
if (ctx.out.json) {
|
|
9542
9596
|
ctx.out.result({ ok: true, url, opened, connector: entry?.type ?? null });
|
|
@@ -9550,6 +9604,33 @@ async function connect(ctx, args) {
|
|
|
9550
9604
|
ctx.out.result({}, url);
|
|
9551
9605
|
return EXIT.ok;
|
|
9552
9606
|
}
|
|
9607
|
+
async function existingConnection(client, type) {
|
|
9608
|
+
try {
|
|
9609
|
+
const body = await client.request(`${PLATFORM2}/connections`);
|
|
9610
|
+
const same = records(Array.isArray(body) ? body : record(body)?.connections).filter((c) => field(c.connector_type) === type || field(record(c.connector)?.type) === type);
|
|
9611
|
+
return same.find((c) => FINE.has(healthOf(c))) ?? same[0] ?? null;
|
|
9612
|
+
} catch {
|
|
9613
|
+
return null;
|
|
9614
|
+
}
|
|
9615
|
+
}
|
|
9616
|
+
function alreadyConnected(ctx, entry, existing, addUrl, pageUrl) {
|
|
9617
|
+
const health = healthOf(existing);
|
|
9618
|
+
const healthy = FINE.has(health);
|
|
9619
|
+
const synced = day(existing.last_synced_at);
|
|
9620
|
+
if (ctx.out.json) {
|
|
9621
|
+
ctx.out.result({ ok: true, url: addUrl, opened: false, connector: entry.type, connected: { id: field(existing.id), health, healthy, url: pageUrl } });
|
|
9622
|
+
return EXIT.ok;
|
|
9623
|
+
}
|
|
9624
|
+
if (healthy) {
|
|
9625
|
+
ctx.out.info(`${entry.name} is already connected and healthy${synced ? ` (last sync ${synced})` : ""}.`);
|
|
9626
|
+
ctx.out.info("To add another one, open this link:");
|
|
9627
|
+
ctx.out.result({}, addUrl);
|
|
9628
|
+
return EXIT.ok;
|
|
9629
|
+
}
|
|
9630
|
+
ctx.out.info(`${entry.name} is already connected, but its health is "${HEALTH[health] ?? health}".`);
|
|
9631
|
+
ctx.out.info(`${health === "needs_reauth" ? "Sign in to it again" : "Fix it"}: ${pageUrl}`);
|
|
9632
|
+
return EXIT.ok;
|
|
9633
|
+
}
|
|
9553
9634
|
function connectionRow(c) {
|
|
9554
9635
|
const connector = record(c.connector);
|
|
9555
9636
|
const type = field(connector?.name) || field(c.connector_type);
|
|
@@ -9602,13 +9683,27 @@ async function fixLine(ctx, credential, attention, slug) {
|
|
|
9602
9683
|
return `Fix them: ${connectionsUrl(consoleUrl, ws)}`;
|
|
9603
9684
|
}
|
|
9604
9685
|
|
|
9605
|
-
// src/
|
|
9686
|
+
// src/files.ts
|
|
9606
9687
|
import { readFileSync as readFileSync4 } from "fs";
|
|
9688
|
+
function readTextFile(path) {
|
|
9689
|
+
try {
|
|
9690
|
+
return readFileSync4(path, "utf8");
|
|
9691
|
+
} catch (err) {
|
|
9692
|
+
const code = err?.code;
|
|
9693
|
+
if (code === "ENOENT") throw usageError(`Can't find ${path}.`);
|
|
9694
|
+
if (code === "EISDIR") throw usageError(`${path} is a folder, not a file.`);
|
|
9695
|
+
if (code === "EACCES" || code === "EPERM") throw usageError(`Can't read ${path}: permission denied.`);
|
|
9696
|
+
throw usageError(`Can't read ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
9697
|
+
}
|
|
9698
|
+
}
|
|
9699
|
+
|
|
9700
|
+
// src/commands/feedback.ts
|
|
9607
9701
|
var PLATFORM3 = "/api/platform/v1";
|
|
9608
9702
|
var FEEDBACK_OUTCOMES = ["used", "used_with_edits", "not_used", "unsure"];
|
|
9609
9703
|
var FEEDBACK_EVIDENCE = ["did_it_myself", "reported_to_me", "inferred"];
|
|
9610
9704
|
var SEND_PROVIDERS = ["lemlist", "instantly", "outreach", "salesloft", "hubspot", "salesforce", "gmail", "heyreach", "apollo", "linkedin_manual", "other"];
|
|
9611
9705
|
var SEND_CHANNELS = ["email", "linkedin_invite", "linkedin_message", "linkedin_inmail"];
|
|
9706
|
+
var CHANNEL_ALIASES = { linkedin: "linkedin_message" };
|
|
9612
9707
|
var SENT_AS = ["returned", "edited"];
|
|
9613
9708
|
var MAX_MESSAGE_CHARS = 1e4;
|
|
9614
9709
|
var MAX_COMMENT_CHARS = 2e3;
|
|
@@ -9633,11 +9728,7 @@ function text(flag, value, max) {
|
|
|
9633
9728
|
return value;
|
|
9634
9729
|
}
|
|
9635
9730
|
async function readText(ctx, path) {
|
|
9636
|
-
|
|
9637
|
-
return path === "-" ? await ctx.io.readStdin() : readFileSync4(path, "utf8");
|
|
9638
|
-
} catch (err) {
|
|
9639
|
-
throw usageError(`Could not read --sent-text ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
9640
|
-
}
|
|
9731
|
+
return path === "-" ? await ctx.io.readStdin() : readTextFile(path);
|
|
9641
9732
|
}
|
|
9642
9733
|
async function feedback(ctx, args) {
|
|
9643
9734
|
const usage = `Usage: ${amdahlCommand("feedback <run-id> --evidence did_it_myself [--outcome used] [--sent-text <file>] [--comment <text>]")}`;
|
|
@@ -9675,21 +9766,28 @@ var PROVIDER_IDS = {
|
|
|
9675
9766
|
"provider-lead-id": ["provider_lead_id", 256],
|
|
9676
9767
|
"thread-id": ["thread_id", 512]
|
|
9677
9768
|
};
|
|
9678
|
-
var SEND_KEYS = ["--provider-send-id", "--recipient", "--recipient-key-hash"];
|
|
9679
9769
|
async function sent(ctx, args) {
|
|
9680
9770
|
const { flags } = ctx;
|
|
9681
9771
|
const usage = `Usage: ${amdahlCommand("sent <run-id> --provider <provider> --channel <channel> --sent-as returned|edited --provider-send-id <id>")}`;
|
|
9682
9772
|
const [rawId, ...rest] = args;
|
|
9683
9773
|
if (!rawId) throw usageError("Give the run id of the message that was sent.", usage);
|
|
9774
|
+
if (rest.length > 0 && rest.every((r) => r.toLowerCase() === rawId.toLowerCase())) {
|
|
9775
|
+
throw usageError(`${rawId} is there twice.`, "Give each run id once. The command reports one send.");
|
|
9776
|
+
}
|
|
9684
9777
|
if (rest.length > 0) {
|
|
9685
9778
|
throw usageError("Report one send at a time: give one run id.", "Each send needs its own --provider-send-id or --recipient, so run it once for each. The API takes up to 100 at once (POST /messages/report_send).");
|
|
9686
9779
|
}
|
|
9687
9780
|
const id = runId(rawId, usage);
|
|
9688
|
-
const
|
|
9689
|
-
|
|
9781
|
+
const rawProvider = str(flags.provider);
|
|
9782
|
+
if (rawProvider !== void 0 && !SEND_PROVIDERS.includes(rawProvider)) {
|
|
9783
|
+
throw usageError(`--provider must be one of: ${SEND_PROVIDERS.join(", ")}.`, `"${clip(rawProvider, 40)}" is not on the list. For a sender that is not, use --provider other.`);
|
|
9784
|
+
}
|
|
9785
|
+
const provider = rawProvider;
|
|
9786
|
+
const rawChannel = str(flags.channel);
|
|
9787
|
+
const channel = rawChannel === void 0 ? void 0 : CHANNEL_ALIASES[rawChannel] ?? oneOf("channel", rawChannel, [...SEND_CHANNELS, ...Object.keys(CHANNEL_ALIASES)]);
|
|
9690
9788
|
const sentAs = oneOf("sent-as", str(flags["sent-as"]), SENT_AS);
|
|
9691
9789
|
if (!provider) throw usageError(`Say where it went out with --provider: ${SEND_PROVIDERS.join(", ")}.`, usage);
|
|
9692
|
-
if (!channel) throw usageError(`Say what was sent with --channel: ${SEND_CHANNELS.join(", ")}.`, usage);
|
|
9790
|
+
if (!channel) throw usageError(`Say what was sent with --channel: ${[...SEND_CHANNELS, ...Object.keys(CHANNEL_ALIASES)].join(", ")}.`, usage);
|
|
9693
9791
|
if (!sentAs) throw usageError("Say whether it was sent as Amdahl returned it with --sent-as: returned, edited.", usage);
|
|
9694
9792
|
const sentAt = str(flags["sent-at"]) ?? new Date(ctx.io.now()).toISOString();
|
|
9695
9793
|
if (!TIME_WITH_ZONE.test(sentAt) || !Number.isFinite(Date.parse(sentAt))) {
|
|
@@ -9726,8 +9824,14 @@ async function sent(ctx, args) {
|
|
|
9726
9824
|
}
|
|
9727
9825
|
const sentPath = str(flags["sent-text"]);
|
|
9728
9826
|
if (sentPath !== void 0) item.sent_text = text("sent-text", await readText(ctx, sentPath), MAX_MESSAGE_CHARS);
|
|
9827
|
+
if (sentAs === "edited" && sentPath === void 0) {
|
|
9828
|
+
throw usageError("--sent-as edited needs --sent-text <file>: the message as you sent it.", "Amdahl keeps only a hash of the text. Use - to read it from standard input.");
|
|
9829
|
+
}
|
|
9830
|
+
if (sentAs === "edited" && !(typeof item.sent_text === "string" && item.sent_text.trim())) {
|
|
9831
|
+
throw usageError(`${sentPath === "-" ? "Standard input" : `--sent-text ${sentPath}`} is empty. An edited send needs the message as you sent it.`);
|
|
9832
|
+
}
|
|
9729
9833
|
if (!sendId?.trim() && !recipient?.trim() && !hash?.trim()) {
|
|
9730
|
-
throw usageError(
|
|
9834
|
+
throw usageError("Say which send this is: give --provider-send-id, a recipient (--recipient or --recipient-key-hash), or both.", "Amdahl matches a report to a send by its provider's own id, by the recipient (stored only as a hash), or both.");
|
|
9731
9835
|
}
|
|
9732
9836
|
const credential = await resolveCredential(ctx);
|
|
9733
9837
|
const result = await call(
|
|
@@ -10247,11 +10351,18 @@ async function keysRevoke(ctx, args) {
|
|
|
10247
10351
|
return EXIT.ok;
|
|
10248
10352
|
}
|
|
10249
10353
|
|
|
10250
|
-
// src/
|
|
10251
|
-
|
|
10354
|
+
// src/next-steps.ts
|
|
10355
|
+
function sentLines(runId2, lead = "Sent it?") {
|
|
10356
|
+
const id = field(runId2);
|
|
10357
|
+
if (!id) return [];
|
|
10358
|
+
return [
|
|
10359
|
+
`${lead} ${amdahlCommand("sent")} ${id} --provider <provider> --channel <channel> --sent-as returned --provider-send-id <id>`,
|
|
10360
|
+
`Or say how it went: ${amdahlCommand("feedback")} ${id} --evidence did_it_myself --outcome used`
|
|
10361
|
+
];
|
|
10362
|
+
}
|
|
10252
10363
|
|
|
10253
10364
|
// src/commands/batch.ts
|
|
10254
|
-
import { existsSync as existsSync5,
|
|
10365
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
10255
10366
|
import { basename as basename2, dirname as dirname4, extname, join as join7 } from "path";
|
|
10256
10367
|
var PLATFORM4 = "/api/platform/v1";
|
|
10257
10368
|
var BATCH_WAIT_MS = 15e3;
|
|
@@ -10329,12 +10440,15 @@ function csvList(path, text3) {
|
|
|
10329
10440
|
const [header, ...body] = rows;
|
|
10330
10441
|
if (!header) throw usageError(`${path} is empty.`);
|
|
10331
10442
|
const names = header.map((h) => h.trim().toLowerCase());
|
|
10332
|
-
const
|
|
10333
|
-
|
|
10334
|
-
if (
|
|
10335
|
-
throw usageError(
|
|
10443
|
+
const twice = CSV_COLUMNS.find((c) => names.indexOf(c) !== names.lastIndexOf(c));
|
|
10444
|
+
if (twice !== void 0) throw usageError(`${path} has the column "${twice}" twice.`);
|
|
10445
|
+
if (!names.includes("message")) {
|
|
10446
|
+
throw usageError(
|
|
10447
|
+
`${path} needs a "message" column.`,
|
|
10448
|
+
`The first row must name the columns. Required: message. Read: ${CSV_COLUMNS.filter((c) => c !== "message").join(", ")}. Any other column is copied to the results as it is.`
|
|
10449
|
+
);
|
|
10336
10450
|
}
|
|
10337
|
-
|
|
10451
|
+
const ignored = header.map((h, i) => ({ name: h.trim(), known: CSV_COLUMNS.includes(names[i] ?? "") })).filter((c) => !c.known && c.name).map((c) => clip(c.name, 30));
|
|
10338
10452
|
const items = body.map((row, i) => {
|
|
10339
10453
|
if (row.length !== names.length) {
|
|
10340
10454
|
throw usageError(`Line ${lines[i + 1]} of ${path} has ${count(row.length, "cell")}; the header has ${names.length}.`);
|
|
@@ -10343,14 +10457,14 @@ function csvList(path, text3) {
|
|
|
10343
10457
|
const context = {};
|
|
10344
10458
|
names.forEach((name, j) => {
|
|
10345
10459
|
const value = row[j] ?? "";
|
|
10346
|
-
if (!value.trim()) return;
|
|
10460
|
+
if (!CSV_COLUMNS.includes(name) || !value.trim()) return;
|
|
10347
10461
|
if (name === "facts" || name === "rules") context[name] = perLine(value);
|
|
10348
10462
|
else item[name] = name === "message" ? value : value.trim();
|
|
10349
10463
|
});
|
|
10350
10464
|
if (Object.keys(context).length > 0) item.context = context;
|
|
10351
10465
|
return item;
|
|
10352
10466
|
});
|
|
10353
|
-
return { path, kind: "csv", items, header, rows: body, lines: lines.slice(1) };
|
|
10467
|
+
return { path, kind: "csv", items, header, rows: body, ignored, lines: lines.slice(1) };
|
|
10354
10468
|
}
|
|
10355
10469
|
function jsonlList(path, text3) {
|
|
10356
10470
|
const items = [];
|
|
@@ -10371,12 +10485,7 @@ function jsonlList(path, text3) {
|
|
|
10371
10485
|
return { path, kind: "jsonl", items, lines };
|
|
10372
10486
|
}
|
|
10373
10487
|
function readList(path) {
|
|
10374
|
-
|
|
10375
|
-
try {
|
|
10376
|
-
text3 = readFileSync6(path, "utf8");
|
|
10377
|
-
} catch (err) {
|
|
10378
|
-
throw usageError(`Could not read ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
10379
|
-
}
|
|
10488
|
+
const text3 = readTextFile(path);
|
|
10380
10489
|
const ext = extname(path).toLowerCase();
|
|
10381
10490
|
const jsonl = ext === ".jsonl" || ext === ".ndjson" || ext !== ".csv" && text3.trimStart().startsWith("{");
|
|
10382
10491
|
const list2 = jsonl ? jsonlList(path, text3) : csvList(path, text3);
|
|
@@ -10398,6 +10507,10 @@ function withDefaults(items, defaults) {
|
|
|
10398
10507
|
return merged;
|
|
10399
10508
|
});
|
|
10400
10509
|
}
|
|
10510
|
+
function progressKey(batch) {
|
|
10511
|
+
const c = record(batch.counts) ?? {};
|
|
10512
|
+
return ["total", "queued", "running", "done", "failed"].map((k) => typeof c[k] === "number" ? c[k] : 0).join("/");
|
|
10513
|
+
}
|
|
10401
10514
|
function progressLine(batch) {
|
|
10402
10515
|
const c = record(batch.counts) ?? {};
|
|
10403
10516
|
const n = (k) => typeof c[k] === "number" ? c[k] : 0;
|
|
@@ -10409,7 +10522,7 @@ function progressLine(batch) {
|
|
|
10409
10522
|
const left = seconds === null || finished === total ? "" : seconds < 90 ? ", about a minute left" : `, about ${Math.round(seconds / 60)} min left`;
|
|
10410
10523
|
if (finished === 0 && n("running") === 0) {
|
|
10411
10524
|
const ahead = typeof batch.queue_position === "number" && batch.queue_position > 0 ? ` behind ${count(batch.queue_position, "item")}` : "";
|
|
10412
|
-
return `Queued${ahead}
|
|
10525
|
+
return `Queued${ahead}${left}`;
|
|
10413
10526
|
}
|
|
10414
10527
|
return `${n("done")} of ${total} done${failed}${left}`;
|
|
10415
10528
|
}
|
|
@@ -10417,12 +10530,14 @@ async function waitForBatch(ctx, credential, id, first2) {
|
|
|
10417
10530
|
const client = apiClient(ctx, credential);
|
|
10418
10531
|
const tty = ctx.io.stderrIsTTY && !ctx.out.json;
|
|
10419
10532
|
let last = "";
|
|
10533
|
+
let lastKey = "";
|
|
10420
10534
|
const show = (batch2) => {
|
|
10421
10535
|
const line = progressLine(batch2);
|
|
10422
10536
|
if (line === last) return;
|
|
10423
10537
|
last = line;
|
|
10424
10538
|
if (tty) ctx.io.stderr(`\r\x1B[2K${line}`);
|
|
10425
|
-
else ctx.out.info(line);
|
|
10539
|
+
else if (progressKey(batch2) !== lastKey) ctx.out.info(line);
|
|
10540
|
+
lastKey = progressKey(batch2);
|
|
10426
10541
|
};
|
|
10427
10542
|
let batch = first2;
|
|
10428
10543
|
for (; ; ) {
|
|
@@ -10454,13 +10569,22 @@ async function allItems(ctx, credential, id) {
|
|
|
10454
10569
|
function neutralise(cell2) {
|
|
10455
10570
|
return /^[=+\-@\t\r]/.test(cell2) ? `'${cell2}` : cell2;
|
|
10456
10571
|
}
|
|
10457
|
-
var RESULT_COLUMNS = ["optimized_message", "status", "
|
|
10458
|
-
function
|
|
10572
|
+
var RESULT_COLUMNS = ["optimized_message", "status", "summary", "run_id", "error"];
|
|
10573
|
+
function resultColumnNames(header) {
|
|
10574
|
+
const taken = new Set(header.map((h) => h.trim().toLowerCase()));
|
|
10575
|
+
return RESULT_COLUMNS.map((name) => {
|
|
10576
|
+
let candidate = name;
|
|
10577
|
+
while (taken.has(candidate)) candidate = `amdahl_${candidate}`;
|
|
10578
|
+
taken.add(candidate);
|
|
10579
|
+
return candidate;
|
|
10580
|
+
});
|
|
10581
|
+
}
|
|
10582
|
+
function resultCells(item, context) {
|
|
10459
10583
|
const result = record(item?.result);
|
|
10460
10584
|
if (!item || !result) return ["", field(item?.status) || "missing", "", "", ""];
|
|
10461
10585
|
if (result.ok === false) return ["", "failed", "", field(result.run_id), `${field(result.reason)}: ${field(result.detail)}`.replace(/: $/, "")];
|
|
10462
10586
|
const message2 = typeof result.message === "string" ? result.message : "";
|
|
10463
|
-
return [message2, result.unchanged === true ? "unchanged" : "done", field(result.
|
|
10587
|
+
return [message2, result.unchanged === true ? "unchanged" : "done", truthfulSummary(field(result.summary), context), field(result.run_id), ""];
|
|
10464
10588
|
}
|
|
10465
10589
|
function resultsFile(list2, id, cwd) {
|
|
10466
10590
|
if (list2) return resultsPath(list2.path, list2.kind);
|
|
@@ -10469,12 +10593,12 @@ function resultsFile(list2, id, cwd) {
|
|
|
10469
10593
|
function assertFree(path, force) {
|
|
10470
10594
|
if (!force && existsSync5(path)) throw usageError(`${path} already exists.`, "Move it, or add --force to replace it.");
|
|
10471
10595
|
}
|
|
10472
|
-
function writeResults(list2, id, items, cwd, force) {
|
|
10596
|
+
function writeResults(list2, id, items, cwd, force, contexts) {
|
|
10473
10597
|
const byIndex = new Map(items.map((i) => [i.index, i]));
|
|
10474
10598
|
const path = resultsFile(list2, id, cwd);
|
|
10475
10599
|
const flag = force ? "w" : "wx";
|
|
10476
10600
|
if (list2?.kind === "csv" && list2.header && list2.rows) {
|
|
10477
|
-
const rows = [[...list2.header, ...
|
|
10601
|
+
const rows = [[...list2.header, ...resultColumnNames(list2.header)], ...list2.rows.map((row, i) => [...row, ...resultCells(byIndex.get(i), contexts[i])])];
|
|
10478
10602
|
writeFileSync4(path, toCsv(rows.map((row) => row.map(neutralise))), { flag });
|
|
10479
10603
|
return path;
|
|
10480
10604
|
}
|
|
@@ -10500,6 +10624,11 @@ async function optimizeList(ctx, defaults) {
|
|
|
10500
10624
|
throw usageError("--batch needs a batch id: a UUID like the one `amdahl optimize --list` printed.");
|
|
10501
10625
|
}
|
|
10502
10626
|
const list2 = listPath ? readList(listPath) : null;
|
|
10627
|
+
if (list2?.ignored?.length) {
|
|
10628
|
+
const shown = list2.ignored.slice(0, 6).join(", ");
|
|
10629
|
+
const more = list2.ignored.length > 6 ? ` and ${list2.ignored.length - 6} more` : "";
|
|
10630
|
+
out.info(`Not sent, and copied to the results as they are: ${count(list2.ignored.length, "column")} Amdahl does not read (${shown}${more}).`);
|
|
10631
|
+
}
|
|
10503
10632
|
if (!resume && list2 && list2.items.length > MAX_BATCH_ITEMS) {
|
|
10504
10633
|
throw usageError(`A batch takes at most ${MAX_BATCH_ITEMS} messages, and ${list2.path} has ${list2.items.length}.`, "Split the list into files of 500 or fewer, and run each one.");
|
|
10505
10634
|
}
|
|
@@ -10546,10 +10675,13 @@ Get the results: ${pickUp}`);
|
|
|
10546
10675
|
out.result({ ...batch, ok: failed === 0, items, next_cursor: null });
|
|
10547
10676
|
return failed === 0 ? EXIT.ok : EXIT.general;
|
|
10548
10677
|
}
|
|
10549
|
-
const
|
|
10678
|
+
const contexts = list2 ? withDefaults(list2.items, defaults).map((item) => item.context) : [];
|
|
10679
|
+
const path = writeResults(list2, id, items, ctx.io.cwd, force, contexts);
|
|
10550
10680
|
const done = items.length - failed;
|
|
10551
10681
|
out.info(`${done} of ${count(items.length, "message")} optimized${failed ? `, ${failed} failed` : ""}.`);
|
|
10552
10682
|
out.result({}, `Results: ${path}`);
|
|
10683
|
+
const runId2 = items.map((i) => field(record(i.result)?.run_id)).find(Boolean);
|
|
10684
|
+
for (const line of sentLines(runId2, "Sent one?")) out.info(line);
|
|
10553
10685
|
return failed === 0 ? EXIT.ok : EXIT.general;
|
|
10554
10686
|
}
|
|
10555
10687
|
|
|
@@ -10565,11 +10697,12 @@ function oneOf2(flag, value, allowed) {
|
|
|
10565
10697
|
return value;
|
|
10566
10698
|
}
|
|
10567
10699
|
function readContext(path) {
|
|
10700
|
+
const text3 = readTextFile(path);
|
|
10568
10701
|
let parsed;
|
|
10569
10702
|
try {
|
|
10570
|
-
parsed = JSON.parse(
|
|
10703
|
+
parsed = JSON.parse(text3);
|
|
10571
10704
|
} catch (err) {
|
|
10572
|
-
throw usageError(
|
|
10705
|
+
throw usageError(`--context ${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
10573
10706
|
}
|
|
10574
10707
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10575
10708
|
throw usageError("--context must be a JSON object with rules, voice_examples and facts.");
|
|
@@ -10688,7 +10821,6 @@ function whatAmdahlDid(result) {
|
|
|
10688
10821
|
const more = whys.length > 1 ? ` (${plural(whys.length - 1, "more reason")} in --json)` : "";
|
|
10689
10822
|
const leftOut = omitted.length ? `Left out ${plural(omitted.length, "thing")}${whys[0] ? `: ${sentence(whys[0])}` : "."}${more}` : "";
|
|
10690
10823
|
const lines = [
|
|
10691
|
-
{ rank: 2, line: sentence(text2(result.what_changed)) },
|
|
10692
10824
|
{ rank: 5, line: checked },
|
|
10693
10825
|
{ rank: 3, line: removed },
|
|
10694
10826
|
{ rank: 6, line: usedLine },
|
|
@@ -10800,7 +10932,7 @@ ${nextTimeLine(ctx)}`);
|
|
|
10800
10932
|
out.result({}, String(result.message ?? ""));
|
|
10801
10933
|
const did = whatAmdahlDid(result);
|
|
10802
10934
|
const connect2 = tipsOn(ctx) && (saysNoData(result) || await noData) ? [noDataLine()] : [];
|
|
10803
|
-
out.info([...did, ...feedbackLines(ctx, result), ...connect2, nextTimeLine(ctx)].join("\n"));
|
|
10935
|
+
out.info([...did, ...feedbackLines(ctx, result), ...sentLines(result.run_id), ...connect2, nextTimeLine(ctx)].join("\n"));
|
|
10804
10936
|
return EXIT.ok;
|
|
10805
10937
|
}
|
|
10806
10938
|
async function optimize(ctx, args) {
|
|
@@ -10826,12 +10958,7 @@ async function optimize(ctx, args) {
|
|
|
10826
10958
|
const base = requestOptions(ctx);
|
|
10827
10959
|
const inputs = [];
|
|
10828
10960
|
for (const file of files) {
|
|
10829
|
-
|
|
10830
|
-
try {
|
|
10831
|
-
message2 = file === "-" ? await io.readStdin() : readFileSync7(file, "utf8");
|
|
10832
|
-
} catch (err) {
|
|
10833
|
-
throw usageError(`Could not read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
10834
|
-
}
|
|
10961
|
+
const message2 = file === "-" ? await io.readStdin() : readTextFile(file);
|
|
10835
10962
|
if (!message2.trim()) throw usageError(`${file === "-" ? "Stdin" : file} is empty.`);
|
|
10836
10963
|
inputs.push({ file, message: message2 });
|
|
10837
10964
|
}
|
|
@@ -10851,9 +10978,9 @@ async function optimize(ctx, args) {
|
|
|
10851
10978
|
if (!out.json) {
|
|
10852
10979
|
if (inputs.length > 1) out.info(`== ${file} ==`);
|
|
10853
10980
|
if (result.unchanged === true) out.info("No rewrite beat your draft; it is returned unchanged.");
|
|
10854
|
-
else if (typeof result.summary === "string") out.info(result.summary);
|
|
10981
|
+
else if (typeof result.summary === "string" && truthfulSummary(result.summary, base.context)) out.info(truthfulSummary(result.summary, base.context));
|
|
10855
10982
|
out.result({}, String(result.message ?? ""));
|
|
10856
|
-
const lines = [...whatAmdahlDid(result), ...feedbackLines(ctx, result)];
|
|
10983
|
+
const lines = [...whatAmdahlDid(result), ...feedbackLines(ctx, result), ...sentLines(result.run_id)];
|
|
10857
10984
|
if (lines.length) out.info(lines.join("\n"));
|
|
10858
10985
|
}
|
|
10859
10986
|
}
|
|
@@ -11005,13 +11132,16 @@ var RESEARCH_TIMEOUT_MS = 15 * 6e4;
|
|
|
11005
11132
|
var ROWS_UNDER_ANSWER = 5;
|
|
11006
11133
|
var ROWS_ALONE = 10;
|
|
11007
11134
|
function researchKeyLine() {
|
|
11008
|
-
return `
|
|
11135
|
+
return `Research needs a Customer agent key: ${amdahlCommand("keys create --name research --preset agent")}`;
|
|
11136
|
+
}
|
|
11137
|
+
function researchKeyFirst() {
|
|
11138
|
+
return `${researchKeyLine()}. Here is the search answer instead.`;
|
|
11009
11139
|
}
|
|
11010
11140
|
var SETTLED = /* @__PURE__ */ new Set(["complete", "failed", "canceled", "needs_input"]);
|
|
11011
11141
|
var TITLE_FIELDS = ["title", "call_title", "meeting_title", "subject", "deal_name", "account_name", "company_name", "name"];
|
|
11012
11142
|
var DATE_FIELDS = ["occurred_at", "started_at", "date", "call_date", "call_timestamp", "close_date", "created_at", "updated_at"];
|
|
11013
|
-
var TEXT_FIELDS = ["text", "quote", "snippet", "excerpt", "content", "summary", "body", "description"];
|
|
11014
|
-
var WHO_FIELDS = ["speaker", "speaker_name", "account_name", "company_name", "owner_name", "deal_stage_label"];
|
|
11143
|
+
var TEXT_FIELDS = ["content_preview", "text", "quote", "snippet", "excerpt", "content", "summary", "body", "description"];
|
|
11144
|
+
var WHO_FIELDS = ["speaker", "speaker_name", "account_name", "company_name", "owner_name", "deal_stage_label", "speaker_type"];
|
|
11015
11145
|
var AMOUNT_FIELDS = ["deal_amount", "amount"];
|
|
11016
11146
|
function first(row, names) {
|
|
11017
11147
|
for (const name of names) {
|
|
@@ -11039,15 +11169,24 @@ function rowLines(row, n) {
|
|
|
11039
11169
|
function rowsOf(result) {
|
|
11040
11170
|
return records(result.results);
|
|
11041
11171
|
}
|
|
11172
|
+
function noAnswerLines(status2, rows) {
|
|
11173
|
+
if (rows === 0) return [];
|
|
11174
|
+
if (status2 === "not_enough_data") {
|
|
11175
|
+
return [...wrap(`Amdahl couldn't answer that from these rows. Try rephrasing the question, or run ${amdahlCommand("connections")} to see whether your data is synced.`), ""];
|
|
11176
|
+
}
|
|
11177
|
+
if (status2 === "failed") return [...wrap(`Amdahl found rows but couldn't write an answer this time. Here are the rows. Try again, or rephrase the question.`), ""];
|
|
11178
|
+
return [];
|
|
11179
|
+
}
|
|
11042
11180
|
function searchLines(result) {
|
|
11043
11181
|
const lines = [];
|
|
11044
11182
|
const rows = rowsOf(result);
|
|
11045
11183
|
const answer = record(result.answer);
|
|
11046
11184
|
const answered = answer?.status === "answered" && paragraphs(answer.text);
|
|
11047
11185
|
if (answered) lines.push(...wrap(plainMarkdown(answered)), "");
|
|
11186
|
+
else lines.push(...noAnswerLines(field(answer?.status), rows.length));
|
|
11048
11187
|
const shown = rows.slice(0, answered ? ROWS_UNDER_ANSWER : ROWS_ALONE);
|
|
11049
11188
|
if (shown.length > 0) {
|
|
11050
|
-
if (answered) lines.push("From:");
|
|
11189
|
+
if (answered || lines.length > 0) lines.push(answered ? "From:" : "Rows it read:");
|
|
11051
11190
|
shown.forEach((row, i) => lines.push(...rowLines(row, i + 1)));
|
|
11052
11191
|
if (rows.length > shown.length) lines.push(`And ${count(rows.length - shown.length, "more row")} (all of them: add --json).`);
|
|
11053
11192
|
} else if (!answered) {
|
|
@@ -11126,13 +11265,12 @@ async function search(ctx, args) {
|
|
|
11126
11265
|
out.result({}, [...wrap(field(setup2.message) || "This workspace has no data connected yet."), connectLine()].join("\n"));
|
|
11127
11266
|
return EXIT.ok;
|
|
11128
11267
|
}
|
|
11129
|
-
|
|
11130
|
-
out.result({}, lines.join("\n"));
|
|
11131
|
-
if (refusedForScope(notes)) out.info(researchKeyLine());
|
|
11268
|
+
if (refusedForScope(notes)) out.info(wrap(researchKeyFirst()).join("\n"));
|
|
11132
11269
|
else {
|
|
11133
11270
|
const refused = notes.find((n) => /ran search instead/i.test(n));
|
|
11134
11271
|
if (refused) out.info(wrap(refused).join("\n"));
|
|
11135
11272
|
}
|
|
11273
|
+
out.result({}, searchLines(result).join("\n"));
|
|
11136
11274
|
return EXIT.ok;
|
|
11137
11275
|
}
|
|
11138
11276
|
function printResearch(ctx, search2, job) {
|
|
@@ -12112,6 +12250,262 @@ async function workspaceUse(ctx, args) {
|
|
|
12112
12250
|
return EXIT.ok;
|
|
12113
12251
|
}
|
|
12114
12252
|
|
|
12253
|
+
// src/help.ts
|
|
12254
|
+
var GLOBAL = `
|
|
12255
|
+
Global options: --profile, --api-url, --api-key, --json, --no-color, --verbose.
|
|
12256
|
+
Run \`amdahl --help\` for what each does.
|
|
12257
|
+
`;
|
|
12258
|
+
var HELP = {
|
|
12259
|
+
setup: `Usage: amdahl [setup] [--no-browser]
|
|
12260
|
+
|
|
12261
|
+
The guided setup. In a terminal it asks where Amdahl should work: Claude Code,
|
|
12262
|
+
Cursor, Codex or this terminal. Run by an agent, or with no terminal, it sets
|
|
12263
|
+
up that agent (or every client it finds) and asks nothing.
|
|
12264
|
+
|
|
12265
|
+
--no-browser Print the sign-in link instead of opening a browser.
|
|
12266
|
+
`,
|
|
12267
|
+
login: `Usage: amdahl login [--workspace <slug>] [--no-browser] [--port <n>]
|
|
12268
|
+
|
|
12269
|
+
Sign in through the browser with your console account. Also: amdahl auth login.
|
|
12270
|
+
|
|
12271
|
+
--workspace <slug> Sign in to this workspace.
|
|
12272
|
+
--no-browser Print the sign-in link instead of opening a browser.
|
|
12273
|
+
--port <n> Listen for the browser's answer on this local port.
|
|
12274
|
+
`,
|
|
12275
|
+
logout: `Usage: amdahl logout [--all]
|
|
12276
|
+
|
|
12277
|
+
Sign out and forget the sign-in on this machine. Also: amdahl auth logout.
|
|
12278
|
+
|
|
12279
|
+
--all Sign out of every saved profile, not just the current one.
|
|
12280
|
+
`,
|
|
12281
|
+
whoami: `Usage: amdahl whoami [--show-token]
|
|
12282
|
+
|
|
12283
|
+
Show who you are: the account, workspace, role and which credential won. Also:
|
|
12284
|
+
amdahl auth status.
|
|
12285
|
+
|
|
12286
|
+
--show-token Print the full access token, not its first characters.
|
|
12287
|
+
`,
|
|
12288
|
+
"auth token": `Usage: amdahl auth token
|
|
12289
|
+
|
|
12290
|
+
Print a fresh access token from your sign-in. It needs your own sign-in: a key
|
|
12291
|
+
in AMDAHL_KEY or --api-key wins over it, and then this exits 4.
|
|
12292
|
+
`,
|
|
12293
|
+
status: `Usage: amdahl status
|
|
12294
|
+
|
|
12295
|
+
Can this workspace optimize, and what blocks it. It shows your role, the
|
|
12296
|
+
monthly optimize quota and any connection that needs attention. Each blocked
|
|
12297
|
+
item names its fix: the command, the console page, or who to ask.
|
|
12298
|
+
|
|
12299
|
+
With --json it prints the server's setup status as one object.
|
|
12300
|
+
`,
|
|
12301
|
+
optimize: `Usage: amdahl optimize [<file>... | -] [--channel email|linkedin]
|
|
12302
|
+
[--evidence off|workspace] [--on-unsupported flag|remove]
|
|
12303
|
+
[--tries] [--context <file.json>]
|
|
12304
|
+
amdahl optimize --list <file.csv|file.jsonl> [--no-wait] [--force]
|
|
12305
|
+
amdahl optimize --batch <id> [--list <file>] [--force]
|
|
12306
|
+
|
|
12307
|
+
Rewrite an outbound email or LinkedIn message. Each file is one message; "-"
|
|
12308
|
+
reads standard input. With no file, at a terminal, paste the message.
|
|
12309
|
+
|
|
12310
|
+
--channel email|linkedin What the message is. Left out, it is inferred.
|
|
12311
|
+
--evidence off|workspace "workspace" also evaluates each claim against
|
|
12312
|
+
your customers' own conversations. Slower.
|
|
12313
|
+
--on-unsupported flag|remove What to do with a claim nothing backs.
|
|
12314
|
+
--tries Also show every version the run scored.
|
|
12315
|
+
--context <file.json> Your rules, voice_examples and facts, as JSON.
|
|
12316
|
+
--list <file> Rewrite a whole list in one batch: a CSV with a
|
|
12317
|
+
message column (it may also have item_id,
|
|
12318
|
+
channel, evidence, on_unsupported, facts and
|
|
12319
|
+
rules; any other column is copied to the
|
|
12320
|
+
results) or a JSONL file, one item per line.
|
|
12321
|
+
Up to 500 messages. The results go next to the
|
|
12322
|
+
list: drafts.csv gets drafts.optimized.csv.
|
|
12323
|
+
--no-wait With --list: queue the batch and print its id.
|
|
12324
|
+
--batch <id> Pick up a batch queued earlier. With --list, the
|
|
12325
|
+
same list; the results go next to it.
|
|
12326
|
+
--force Replace a results file that already exists.
|
|
12327
|
+
|
|
12328
|
+
In the results CSV, a cell that starts with =, +, - or @ gets a ' in front, so
|
|
12329
|
+
a spreadsheet does not run it as a formula. That includes a phone number such
|
|
12330
|
+
as +1 555 0100 or a negative number you copied through. Delete the ' to use it.
|
|
12331
|
+
|
|
12332
|
+
Output: the rewrite goes to stdout. What Amdahl did, and the run id, go to
|
|
12333
|
+
stderr. With --json it prints {"ok", "file", "result"}: the rewritten message
|
|
12334
|
+
is .result.message and the run id is .result.run_id. Several files give an
|
|
12335
|
+
array of those. A list prints the batch, and each item's result, with --json.
|
|
12336
|
+
Exit 1 when a message could not be rewritten.
|
|
12337
|
+
${GLOBAL}`,
|
|
12338
|
+
search: `Usage: amdahl search "<question>" [--research] [--limit <n>]
|
|
12339
|
+
|
|
12340
|
+
Ask a question about your customers, calls and deals. It prints a short answer
|
|
12341
|
+
with the rows it came from.
|
|
12342
|
+
|
|
12343
|
+
--research Start a research job: a deeper, cited answer. It needs a
|
|
12344
|
+
Customer agent key (amdahl keys create --preset agent). With a
|
|
12345
|
+
sign-in you get the search answer and a line that says so.
|
|
12346
|
+
--limit <n> Ask for at most n rows.
|
|
12347
|
+
|
|
12348
|
+
With --json it prints the server's whole answer, every row included.
|
|
12349
|
+
${GLOBAL}`,
|
|
12350
|
+
connections: `Usage: amdahl connections [--all]
|
|
12351
|
+
|
|
12352
|
+
List the sources this workspace has connected, with each one's health and last
|
|
12353
|
+
sync. A source that needs attention gets a link to its console page.
|
|
12354
|
+
|
|
12355
|
+
--all Also list disconnected and never-synced sources.
|
|
12356
|
+
`,
|
|
12357
|
+
connect: `Usage: amdahl connect [<connector>] [--no-browser]
|
|
12358
|
+
|
|
12359
|
+
Open the console's add page. The connector only fills its search box: you pick
|
|
12360
|
+
it there and sign in to that tool. If that connector is already connected and
|
|
12361
|
+
healthy, this says so and prints the page instead of opening it. The CLI never
|
|
12362
|
+
creates a connection itself.
|
|
12363
|
+
|
|
12364
|
+
--no-browser Print the link instead of opening a browser. It also prints
|
|
12365
|
+
under --json, for an agent, in CI, and when stdin or stdout
|
|
12366
|
+
is not a terminal.
|
|
12367
|
+
`,
|
|
12368
|
+
open: `Usage: amdahl open [--no-browser]
|
|
12369
|
+
|
|
12370
|
+
Open this workspace in the console.
|
|
12371
|
+
|
|
12372
|
+
--no-browser Print the link instead of opening a browser.
|
|
12373
|
+
`,
|
|
12374
|
+
feedback: `Usage: amdahl feedback <run-id> --evidence did_it_myself|reported_to_me|inferred
|
|
12375
|
+
[--outcome used|used_with_edits|not_used|unsure]
|
|
12376
|
+
[--sent-text <file> | -] [--comment <text>]
|
|
12377
|
+
|
|
12378
|
+
Say what happened to the message an optimize run returned. The run id is the
|
|
12379
|
+
one \`amdahl optimize\` printed. Amdahl learns your preferences from an edit,
|
|
12380
|
+
unless the evidence is "inferred". Each call stores one event.
|
|
12381
|
+
|
|
12382
|
+
--evidence <how> How you know. Required.
|
|
12383
|
+
--outcome <what> What happened to it.
|
|
12384
|
+
--sent-text <file> The text as sent (a file, or - for standard input).
|
|
12385
|
+
--comment <text> Your own words about it.
|
|
12386
|
+
|
|
12387
|
+
Give at least one of --outcome, --sent-text and --comment.
|
|
12388
|
+
${GLOBAL}`,
|
|
12389
|
+
sent: `Usage: amdahl sent <run-id> --provider <provider> --channel <channel>
|
|
12390
|
+
--sent-as returned|edited [--sent-at <time>]
|
|
12391
|
+
[--provider-send-id <id>]
|
|
12392
|
+
[--recipient <email|url> | --recipient-key-hash <sha256>]
|
|
12393
|
+
[--provider-campaign-id <id>] [--sequence-step <n>]
|
|
12394
|
+
[--provider-variant-id <id>] [--provider-lead-id <id>]
|
|
12395
|
+
[--thread-id <id>] [--sent-text <file> | -]
|
|
12396
|
+
|
|
12397
|
+
Report that the system that sent a message sent it. One send per command; the
|
|
12398
|
+
API takes up to 100 (POST /messages/report_send).
|
|
12399
|
+
|
|
12400
|
+
--provider <name> ${SEND_PROVIDERS.slice(0, 4).join(", ")},
|
|
12401
|
+
${SEND_PROVIDERS.slice(4, 8).join(", ")},
|
|
12402
|
+
${SEND_PROVIDERS.slice(8).join(", ")}.
|
|
12403
|
+
A sender not on the list: use "other".
|
|
12404
|
+
--channel <kind> email, linkedin (the same as linkedin_message),
|
|
12405
|
+
linkedin_invite, linkedin_message or linkedin_inmail.
|
|
12406
|
+
--sent-as <how> returned (as Amdahl returned it) or edited. An edit needs
|
|
12407
|
+
--sent-text: the message as you sent it.
|
|
12408
|
+
--sent-at <time> A time with a zone, like 2026-11-12T09:30:00Z. Default:
|
|
12409
|
+
now.
|
|
12410
|
+
--provider-send-id The provider's own id for the send. For gmail, the
|
|
12411
|
+
Message-ID header with its angle brackets.
|
|
12412
|
+
--recipient An email address, or a LinkedIn profile URL for a LinkedIn
|
|
12413
|
+
channel. Amdahl stores only a hash of it.
|
|
12414
|
+
--recipient-key-hash A SHA-256 you made, instead of --recipient (not both).
|
|
12415
|
+
--provider-campaign-id, --sequence-step, --provider-variant-id,
|
|
12416
|
+
--provider-lead-id, --thread-id The provider's ids, when you have them.
|
|
12417
|
+
--sent-text <file> The text as sent (a file, or - for standard input). Only
|
|
12418
|
+
a hash of it is kept.
|
|
12419
|
+
|
|
12420
|
+
Give at least one of --provider-send-id and a recipient (--recipient or
|
|
12421
|
+
--recipient-key-hash, not both). A send id with a recipient is fine: Amdahl
|
|
12422
|
+
matches a report to a send by the id, the recipient, or both.
|
|
12423
|
+
${GLOBAL}`,
|
|
12424
|
+
"keys create": `Usage: amdahl keys create --name <name>
|
|
12425
|
+
[--preset read-only|agent|internal|admin]
|
|
12426
|
+
[--expires 30d|90d|365d] [--no-browser]
|
|
12427
|
+
|
|
12428
|
+
Make an API key for a server or CI. You approve it in the console, then the key
|
|
12429
|
+
is printed once. It needs your own sign-in.
|
|
12430
|
+
|
|
12431
|
+
--name <name> What to call the key. Required.
|
|
12432
|
+
--preset <kind> read-only (default), agent (reads, optimize and research),
|
|
12433
|
+
internal or admin (workspace admins only).
|
|
12434
|
+
--expires <days> 30d, 90d (default) or 365d.
|
|
12435
|
+
--no-browser Print the approval link instead of opening a browser.
|
|
12436
|
+
`,
|
|
12437
|
+
"keys list": `Usage: amdahl keys list
|
|
12438
|
+
|
|
12439
|
+
List the workspace's keys, without their secrets. It needs your own sign-in.
|
|
12440
|
+
`,
|
|
12441
|
+
"keys revoke": `Usage: amdahl keys revoke <id|prefix> [--yes] [--no-browser]
|
|
12442
|
+
|
|
12443
|
+
Revoke a key. You type the key's name, then approve in the console. It needs
|
|
12444
|
+
your own sign-in.
|
|
12445
|
+
|
|
12446
|
+
--yes Skip the question that asks you to type the name.
|
|
12447
|
+
--no-browser Print the approval link instead of opening a browser.
|
|
12448
|
+
`,
|
|
12449
|
+
"workspace list": `Usage: amdahl workspace list
|
|
12450
|
+
|
|
12451
|
+
List your saved sign-ins (profiles). One sign-in is one workspace.
|
|
12452
|
+
`,
|
|
12453
|
+
"workspace use": `Usage: amdahl workspace use <profile>
|
|
12454
|
+
|
|
12455
|
+
Make a saved profile the default.
|
|
12456
|
+
`,
|
|
12457
|
+
install: `Usage: amdahl install claude-code|codex|cursor [--print] [--force]
|
|
12458
|
+
|
|
12459
|
+
Add the Amdahl MCP server and the Amdahl skill to one AI client. It writes no
|
|
12460
|
+
key or token: the client signs in itself the first time it connects.
|
|
12461
|
+
|
|
12462
|
+
--print Show what it would change, and change nothing.
|
|
12463
|
+
--force Replace a skill file you have edited.
|
|
12464
|
+
`,
|
|
12465
|
+
"skills add": `Usage: amdahl skills add <publisher>/<skill> [-y] [-g] [--agents] [--force]
|
|
12466
|
+
[--accept-warning] [--site <origin>]
|
|
12467
|
+
|
|
12468
|
+
Add a skill from https://amdahl.ai/skills to this project. It needs no sign-in.
|
|
12469
|
+
In a terminal it asks where to install it.
|
|
12470
|
+
|
|
12471
|
+
-y, --yes Add it to this project without asking.
|
|
12472
|
+
-g, --global Add it for every project (~/.claude/skills).
|
|
12473
|
+
--agents Also write .agents/skills, for Codex, Cursor and others.
|
|
12474
|
+
--force Replace a different copy that is already there.
|
|
12475
|
+
--accept-warning Install a skill amdahl.ai's security review flagged,
|
|
12476
|
+
after printing the warning. Without it, -y, --json or no
|
|
12477
|
+
terminal stops (exit 9).
|
|
12478
|
+
--site <origin> Read from another origin (https, or http for localhost).
|
|
12479
|
+
`,
|
|
12480
|
+
help: `Usage: amdahl help [<command>]
|
|
12481
|
+
|
|
12482
|
+
Show the help for one command, or all of them. Also: amdahl <command> --help.
|
|
12483
|
+
`
|
|
12484
|
+
};
|
|
12485
|
+
var GROUPS = {
|
|
12486
|
+
keys: ["keys create", "keys list", "keys revoke"],
|
|
12487
|
+
auth: ["login", "logout", "whoami", "auth token"],
|
|
12488
|
+
workspace: ["workspace list", "workspace use"],
|
|
12489
|
+
skills: ["skills add"]
|
|
12490
|
+
};
|
|
12491
|
+
var ALIASES2 = { "auth login": "login", "auth logout": "logout", "auth status": "whoami" };
|
|
12492
|
+
var HELP_COMMANDS = Object.keys(HELP);
|
|
12493
|
+
function commandHelp(words) {
|
|
12494
|
+
const key = ALIASES2[words.join(" ")] ?? words.join(" ");
|
|
12495
|
+
const text3 = HELP[key];
|
|
12496
|
+
if (text3) return text3;
|
|
12497
|
+
const group = words.length === 1 ? GROUPS[words[0] ?? ""] : void 0;
|
|
12498
|
+
if (!group) return null;
|
|
12499
|
+
const lines = group.map((name) => ` ${amdahlCommand(name)}`);
|
|
12500
|
+
return `Usage: amdahl ${words[0]} <command>
|
|
12501
|
+
|
|
12502
|
+
Commands:
|
|
12503
|
+
${lines.join("\n")}
|
|
12504
|
+
|
|
12505
|
+
Run \`amdahl ${words[0]} <command> --help\` for one command.
|
|
12506
|
+
`;
|
|
12507
|
+
}
|
|
12508
|
+
|
|
12115
12509
|
// src/main.ts
|
|
12116
12510
|
var COMMANDS = {
|
|
12117
12511
|
login: (ctx) => login(ctx),
|
|
@@ -12134,13 +12528,25 @@ var COMMANDS = {
|
|
|
12134
12528
|
install,
|
|
12135
12529
|
"skills add": skillsAdd
|
|
12136
12530
|
};
|
|
12531
|
+
function helpFor(words) {
|
|
12532
|
+
return commandHelp(words.slice(0, 2)) ?? commandHelp(words.slice(0, 1)) ?? USAGE;
|
|
12533
|
+
}
|
|
12137
12534
|
var NO_ARGS = /* @__PURE__ */ new Set(["setup", "login", "logout", "whoami", "auth token", "status", "connections", "open", "keys create", "keys list", "workspace list"]);
|
|
12138
12535
|
async function runCli(argv, io) {
|
|
12139
12536
|
setCommandName(io);
|
|
12140
12537
|
const json = argv.includes("--json");
|
|
12141
12538
|
let out = createOutput(io, json, argv.includes("--no-color"));
|
|
12142
12539
|
try {
|
|
12143
|
-
|
|
12540
|
+
let parsed;
|
|
12541
|
+
try {
|
|
12542
|
+
parsed = parseCommandLine(argv);
|
|
12543
|
+
} catch (err) {
|
|
12544
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
12545
|
+
io.stdout(helpFor(helpWords(argv)));
|
|
12546
|
+
return EXIT.ok;
|
|
12547
|
+
}
|
|
12548
|
+
throw err;
|
|
12549
|
+
}
|
|
12144
12550
|
out = createOutput(io, parsed.flags.json === true, parsed.flags["no-color"] === true);
|
|
12145
12551
|
if (parsed.flags.version) {
|
|
12146
12552
|
io.stdout(`${VERSION}
|
|
@@ -12148,7 +12554,7 @@ async function runCli(argv, io) {
|
|
|
12148
12554
|
return EXIT.ok;
|
|
12149
12555
|
}
|
|
12150
12556
|
if (parsed.flags.help || parsed.command === "help") {
|
|
12151
|
-
io.stdout(
|
|
12557
|
+
io.stdout(helpFor(parsed.command === "help" ? parsed.args : parsed.command?.split(" ") ?? []));
|
|
12152
12558
|
return EXIT.ok;
|
|
12153
12559
|
}
|
|
12154
12560
|
if (NO_ARGS.has(parsed.command ?? "") && parsed.args.length > 0) {
|
package/package.json
CHANGED