predictable-ai 0.1.0

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 ADDED
@@ -0,0 +1,265 @@
1
+ # predictable-ai
2
+
3
+ The Predictable agent surface, on the command line. One command per verb in the
4
+ manifest, line-oriented output by default and `--json` for piping.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install -g predictable-ai
10
+ npx predictable-ai --version
11
+ ```
12
+
13
+ The package ships one binary, `predictable`. `npx` runs it without installing it
14
+ globally first. Either way `--version` prints this client's version and the
15
+ agent-surface hash compiled into it — and, once a token is configured, the hash
16
+ the deployment serves, so a client that disagrees with its deployment says so
17
+ rather than failing later on a verb that moved.
18
+
19
+ To work from a checkout instead:
20
+
21
+ ```bash
22
+ cd cli
23
+ npm ci
24
+ npm run build
25
+ node dist/bin.js --version
26
+ ```
27
+
28
+ ## Quickstart
29
+
30
+ 1. **Mint a token.** In the web app, open **Settings → Agent token**
31
+ (`/settings/agent-token`) and mint one. It is shown once.
32
+
33
+ 2. **Export it.** The environment variable is the channel:
34
+
35
+ ```bash
36
+ export PREDICTABLE_TOKEN='paste-the-minted-value-here'
37
+ ```
38
+
39
+ Typing it into an interactive shell leaves it in your history file. The
40
+ config-file channel described below avoids that, and a secret store beats
41
+ both.
42
+
43
+ 3. **Say who you are.** The token smoke-test verb:
44
+
45
+ ```bash
46
+ predictable whoami
47
+ ```
48
+
49
+ 4. **See what the token reaches.** `predictable lists list` in its default text
50
+ mode prints one line per **client** — id and name — and no lists at all, so
51
+ it will not hand you the list id step 5 needs. Ask for the JSON instead,
52
+ where each client carries its `lists`:
53
+
54
+ ```bash
55
+ predictable lists list --json
56
+ ```
57
+
58
+ That is the server's body verbatim, so it arrives on one line, with every
59
+ list's `id`, `name` and `discovery_source` in it. If you have `jq` — a
60
+ separate tool, not something this package installs — the same output reads
61
+ as a table:
62
+
63
+ ```bash
64
+ predictable lists list --json | jq -r '.clients[].lists[] | [.id, .name, .discovery_source] | @tsv'
65
+ ```
66
+
67
+ Either way, copy one of those list ids for the next step.
68
+
69
+ 5. **Preview a search.** The preview prices the run and mints a quote — it
70
+ spends nothing. Only the second step, the one carrying `--quote`, spends:
71
+
72
+ ```bash
73
+ predictable search run --preview --list <list-id>
74
+ ```
75
+
76
+ Not every list is eligible: base search runs only on a list whose
77
+ `discovery_source` is `exa_agent` (step 4 shows it), that is not
78
+ alumni-seeded, and that already has a search query from the describe step —
79
+ any other list is refused with a 400 rather than quoted.
80
+
81
+ `predictable --help` lists every verb and `predictable <command> --help`
82
+ explains one. `--json` prints the server's response body verbatim, so it pipes:
83
+
84
+ ```bash
85
+ predictable lists list --json | jq '.clients[].lists[].name'
86
+ ```
87
+
88
+ ## Authenticate
89
+
90
+ The CLI reads your token from one of two places, in this order:
91
+
92
+ 1. The `PREDICTABLE_TOKEN` environment variable.
93
+ 2. `~/.predictable/config.json`, a file holding `{"token": "..."}`.
94
+
95
+ It never reads a token from the command line. A token typed as an argument is
96
+ saved in your shell history and is visible in `ps` output to every other user on
97
+ the machine, so no command here accepts one. Pass one as a flag and the CLI
98
+ refuses by name and exits 1 — before printing help, before printing a version,
99
+ and before sending anything — rather than ignoring the flag and making an
100
+ anonymous request. Diagnostics never repeat back a value you typed, so a
101
+ credential in the wrong place does not end up in a CI log.
102
+
103
+ To get a token, open **Settings → Agent token** in the web app and mint
104
+ one. It's shown once. Then store it:
105
+
106
+ ```bash
107
+ mkdir -p ~/.predictable
108
+ chmod 700 ~/.predictable
109
+ # Paste the token when the editor opens, as {"token": "<the token>"}
110
+ $EDITOR ~/.predictable/config.json
111
+ chmod 600 ~/.predictable/config.json
112
+ ```
113
+
114
+ Setting `PREDICTABLE_TOKEN` in your environment works too, and is the right
115
+ channel in CI. Prefer a secret store over typing it into an interactive shell,
116
+ where it lands in your history file.
117
+
118
+ By default the CLI talks to the production deployment. Set
119
+ `PREDICTABLE_BASE_URL` to point it at a preview.
120
+
121
+ ## Claude Code and other agents
122
+
123
+ This CLI is the client-facing contract, and Claude Code is meant to drive it the
124
+ way it drives `git`: install the package, put `PREDICTABLE_TOKEN` in the
125
+ environment of the shell it runs in, and ask for the verb you want. Output is
126
+ line-oriented by default and `--json` for structured reads, so an agent can
127
+ parse what comes back. The two-step rule on paid verbs holds under an agent
128
+ exactly as it does under a person: the preview mints a quote, the acting call
129
+ needs `--quote`, and a paid command without one refuses locally and sends
130
+ nothing.
131
+
132
+ For clients that will install nothing, the same manifest is also served as MCP
133
+ tools by a hosted head on the same deployment:
134
+
135
+ - `POST https://www.predictable.ai/api/agent/mcp` — streamable HTTP, stateless,
136
+ a fresh server per request.
137
+ - It authenticates with the same minted agent token, presented as a bearer
138
+ credential by the MCP client's own configuration. The rule this CLI enforces
139
+ holds there too: the credential travels in a header, never in an argument.
140
+ - Both heads register the same verbs, generated from the one manifest, so the
141
+ two cannot drift apart.
142
+
143
+ The criteria coaching is served both ways: `predictable guide criteria` on the
144
+ CLI, and the `agent://guide` resource over MCP.
145
+
146
+ ## Spending credits
147
+
148
+ Paid verbs are two steps. The first prices the work and mints a quote; the
149
+ second spends against that quote:
150
+
151
+ ```bash
152
+ predictable contacts source --preview --list <list-id> # prints a quote id, credits, expiry
153
+ predictable contacts source --quote <quote-id> --list <list-id>
154
+ ```
155
+
156
+ Run the acting command without `--quote` and it refuses before sending
157
+ anything, and tells you the preview command to run.
158
+
159
+ ## Exporting a campaign bundle
160
+
161
+ `export campaign` is the one verb that writes FILES rather than printing
162
+ records. It produces the seven-file bundle for a list:
163
+
164
+ ```bash
165
+ predictable export campaign --list <list-id> --out ./acme-bundle
166
+ ```
167
+
168
+ | File | What it holds |
169
+ | ---- | ------------- |
170
+ | `accounts.csv` | The approved-accounts export, byte for byte as the server sent it. |
171
+ | `contacts.csv` | Every contact on the list. |
172
+ | `sequence.json` | One sequence's detail. |
173
+ | `events.csv` | Per-send rows, paged to exhaustion. |
174
+ | `replies.json` | The list's replies. |
175
+ | `stats.json` | Three reads: the sequences list with its per-sequence stats, billing usage, and the pipeline. Each is recorded as `ok` with its data or `unavailable` with the reason, so a source you were refused never looks like a source that returned nothing. |
176
+ | `manifest.json` | What was exported, the server's exclusion counts, the row counts, the byte size of every other file, and which stats sources it actually carries. Anything a count does not cover is named in `completeness_scope` with the reason. |
177
+
178
+ Add `--sequence <id>` to pick which sequence `sequence.json` describes and to
179
+ narrow `events.csv` to that sequence.
180
+
181
+ **The directory is replaced, and only on success.** The bundle is assembled in
182
+ full before anything is written, then moved into place as the last act. If a
183
+ read fails, a size cap is breached, or the write itself fails partway, `--out`
184
+ is left exactly as it was — there is no partial bundle to mistake for a whole
185
+ one. A bundle over 250,000 rows in any file, or 100 MB in total, exits with
186
+ `bundle_envelope_exceeded` naming the file that broke the cap.
187
+
188
+ The one bound on that guarantee, stated rather than glossed: replacing an
189
+ existing bundle is two moves, and a process **killed** between them leaves
190
+ `--out` absent for an instant. Nothing is lost — the previous bundle is beside
191
+ it under a `.replaced-…` name and a later run puts it back — but a run
192
+ interrupted by `kill -9` can leave you one bundle behind rather than exactly
193
+ where you were. Closing that instant needs an atomic directory swap the
194
+ platform does not portably offer.
195
+
196
+ **The put-back needs a run that reaches the write.** It is the first act of the
197
+ write phase, and the write phase begins only after every read has succeeded. A
198
+ following run that fails earlier — a 500, an offline host — exits with `--out`
199
+ still absent and the previous bundle still under its `.replaced-…` name.
200
+ Nothing is lost on that path either, but it is the next run that reads
201
+ cleanly, not simply the next run, that restores it.
202
+
203
+ **`contacts.csv` is checked against the server's own total** for the list before
204
+ anything is written. A short read is an error, not a smaller file.
205
+
206
+ ### Suppression and `--purpose`
207
+
208
+ By default the accounts export is suppression-filtered. `--purpose
209
+ reporting_history` **skips that filter on purpose** — it is for reporting on
210
+ what you already did, not for choosing who to contact next:
211
+
212
+ ```bash
213
+ predictable export campaign --list <list-id> --out ./acme-bundle --purpose reporting_history
214
+ ```
215
+
216
+ The bundle says so in two places: a note on stderr when it runs, and
217
+ `manifest.json`'s `purpose`. Do not send outreach to a list built from a
218
+ `reporting_history` export.
219
+
220
+ ### A note on spreadsheets
221
+
222
+ Cells that a spreadsheet would treat as a formula are written with a leading
223
+ apostrophe so they open as text. That includes values that only look like
224
+ formulas — a phone number starting `+`, an address starting `-` — so those
225
+ gain an apostrophe too. `manifest.json` counts how many cells were affected.
226
+ The stored data is unchanged; this applies to the exported file only.
227
+
228
+ ## Exit codes
229
+
230
+ | Code | Meaning |
231
+ | ---- | ------- |
232
+ | 0 | Success. |
233
+ | 1 | Bad flags, or the server rejected the request as given. |
234
+ | 2 | The credential was refused (401 or 403). |
235
+ | 3 | Rate limited (429). The server's `Retry-After` is echoed on stderr. |
236
+ | 4 | Server error, or a verb this deployment does not serve yet. |
237
+ | 5 | A paid command needs `--quote`. Nothing was sent. |
238
+
239
+ Records go to stdout. Warnings, errors and hints go to stderr, so a pipeline
240
+ never eats a diagnostic as data.
241
+
242
+ ## Versions
243
+
244
+ `predictable --version` prints this package's version, the agent-surface hash
245
+ compiled into it, and — when a token is configured — the hash the deployment
246
+ serves. When those two hashes differ, this client and that deployment disagree
247
+ about which verbs exist; update the client.
248
+
249
+ ## Development
250
+
251
+ ```bash
252
+ npm ci
253
+ npm run typecheck
254
+ npm test
255
+ npm run build
256
+ ```
257
+
258
+ `src/generated/commands.json` is generated from
259
+ `dashboard/src/lib/agent/manifest.ts` and must never be hand-edited. The parity
260
+ test in `test/parity.test.ts` fails if the command set here and the verb set
261
+ there ever disagree.
262
+
263
+ The end-to-end walk in `scripts/e2e-walk.mjs` runs the five read commands
264
+ against whatever `PREDICTABLE_BASE_URL` names: a local stub in CI, or a
265
+ deployment when you have a token for one.
package/dist/args.js ADDED
@@ -0,0 +1,76 @@
1
+ import { CliError, EXIT } from './errors.js';
2
+ /** Flags every command understands. */
3
+ export const GLOBAL_BOOLEANS = ['json', 'help'];
4
+ /**
5
+ * Split argv into a command name and its flags.
6
+ *
7
+ * `known` is the registered command list, longest match first, so `lists status`
8
+ * wins over a hypothetical `lists`.
9
+ */
10
+ export function parseArgs(argv, known) {
11
+ const words = [];
12
+ let i = 0;
13
+ while (i < argv.length) {
14
+ const token = argv[i];
15
+ if (token === undefined || token.startsWith('-'))
16
+ break;
17
+ words.push(token);
18
+ i += 1;
19
+ }
20
+ const byLength = [...known].sort((a, b) => b.split(' ').length - a.split(' ').length);
21
+ let command = '';
22
+ let consumed = 0;
23
+ for (const candidate of byLength) {
24
+ const parts = candidate.split(' ');
25
+ if (parts.length <= words.length && parts.every((p, n) => words[n] === p)) {
26
+ command = candidate;
27
+ consumed = parts.length;
28
+ break;
29
+ }
30
+ }
31
+ const flags = new Map();
32
+ const booleans = new Set();
33
+ const rest = words.slice(consumed);
34
+ while (i < argv.length) {
35
+ const token = argv[i];
36
+ if (!token.startsWith('--')) {
37
+ if (token === '-h') {
38
+ booleans.add('help');
39
+ i += 1;
40
+ continue;
41
+ }
42
+ rest.push(token);
43
+ i += 1;
44
+ continue;
45
+ }
46
+ const body = token.slice(2);
47
+ if (body.length === 0) {
48
+ i += 1;
49
+ continue;
50
+ }
51
+ const eq = body.indexOf('=');
52
+ if (eq > 0) {
53
+ flags.set(body.slice(0, eq), body.slice(eq + 1));
54
+ i += 1;
55
+ continue;
56
+ }
57
+ const next = argv[i + 1];
58
+ if (next === undefined || next.startsWith('--')) {
59
+ booleans.add(body);
60
+ i += 1;
61
+ continue;
62
+ }
63
+ flags.set(body, next);
64
+ i += 2;
65
+ }
66
+ return { command, flags, booleans, rest };
67
+ }
68
+ /** Read a required flag, or fail with a usage error naming it. */
69
+ export function requireFlag(args, name, placeholder) {
70
+ const value = args.flags.get(name);
71
+ if (value === undefined || value.length === 0) {
72
+ throw new CliError(EXIT.USAGE, `--${name} ${placeholder} is required.`);
73
+ }
74
+ return value;
75
+ }
76
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"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"}
package/dist/bin.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { run } from './run.js';
3
+ async function readStdin() {
4
+ const chunks = [];
5
+ for await (const chunk of process.stdin)
6
+ chunks.push(Buffer.from(chunk));
7
+ return Buffer.concat(chunks).toString('utf8');
8
+ }
9
+ const code = await run({
10
+ argv: process.argv.slice(2),
11
+ env: process.env,
12
+ stdout: (text) => process.stdout.write(text),
13
+ stderr: (text) => process.stderr.write(text),
14
+ readStdin,
15
+ });
16
+ process.exitCode = code;
17
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAE9B,KAAK,UAAU,SAAS;IACtB,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK;QAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;IACxE,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;AAC/C,CAAC;AAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3B,GAAG,EAAE,OAAO,CAAC,GAAG;IAChB,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;IAC5C,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;IAC5C,SAAS;CACV,CAAC,CAAA;AAEF,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAA"}