@curviate/cli 0.22.0 → 0.23.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/CHANGELOG.md +85 -1
- package/README.md +65 -23
- package/dist/{account-QRGWUQXF.js → account-NZZLUI4E.js} +11 -12
- package/dist/{chunk-H6XD3F66.js → chunk-766W25QS.js} +1 -1
- package/dist/{chunk-ZYURL5VK.js → chunk-AHYUM2XQ.js} +93 -1
- package/dist/{chunk-56ORAZJO.js → chunk-LCIUD7S2.js} +1 -1
- package/dist/chunk-LCLYFE54.js +182 -0
- package/dist/{chunk-QJZ3LWOX.js → chunk-RJTG5YWE.js} +1 -1
- package/dist/{chunk-T5LPGAGJ.js → chunk-S6VAMUCC.js} +16 -22
- package/dist/{chunk-CUTMZWL6.js → chunk-SYRFLZ4D.js} +2 -94
- package/dist/{chunk-JA3NNST6.js → chunk-TKWW6BV6.js} +2 -2
- package/dist/{chunk-DMQZEPQE.js → chunk-UEAX6KUB.js} +10 -0
- package/dist/cli.js +107 -68
- package/dist/{comment-MCGXFMN4.js → comment-SXYTJPXI.js} +21 -25
- package/dist/{company-WDDLWP7E.js → company-GZ2QR2CZ.js} +26 -30
- package/dist/{config-FPWWYG7G.js → config-YKQ4QEBI.js} +4 -3
- package/dist/{connect-5OROSY5C.js → connect-3LTKVRGM.js} +15 -18
- package/dist/{feed-SFVYEAYP.js → feed-ZY27EY52.js} +9 -12
- package/dist/{group-HSZY2IJU.js → group-DBE25EID.js} +16 -16
- package/dist/{inbox-L6JBGZIB.js → inbox-E6ME3ROZ.js} +14 -17
- package/dist/{inboxes-UIO74C5Q.js → inboxes-JRX3J32A.js} +10 -13
- package/dist/{job-75ML7R7I.js → job-U5YIJ7KB.js} +19 -22
- package/dist/{login-ZLDDIAFW.js → login-ERQVI27Q.js} +3 -3
- package/dist/{message-DAQHRBSJ.js → message-NPX7UX46.js} +7 -5
- package/dist/{notification-DYJN4ZHN.js → notification-XSSA5QOJ.js} +11 -14
- package/dist/{post-RGDOCGC4.js → post-OSM7JH65.js} +22 -26
- package/dist/{profile-TJS2YAZD.js → profile-LQXOC7HY.js} +25 -28
- package/dist/{recruiter-NSM33IEK.js → recruiter-ZUTWJHO5.js} +34 -38
- package/dist/{sales-nav-5O7FR57S.js → sales-nav-LSUBIJWZ.js} +22 -25
- package/dist/{search-TDSLK2MB.js → search-FEHXVTQ3.js} +30 -33
- package/dist/{webhook-7ESDSIRU.js → webhook-C43C5BK7.js} +5 -10
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,91 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
6
6
|
Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html):
|
|
7
7
|
a new command or flag is a minor; a breaking command/flag/exit-code change is a major; a fix is a patch.
|
|
8
8
|
|
|
9
|
-
## [
|
|
9
|
+
## [0.23.1] - 2026-08-10
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **A global flag placed before the subcommand was rejected, and any flag
|
|
14
|
+
value beginning with `-` was rejected.** `curviate --api-key <key> account
|
|
15
|
+
list` exited 2 with "unknown command", and a value like `--api-key
|
|
16
|
+
-something` exited 2 with "unknown flag". Both shared one root cause: the
|
|
17
|
+
dispatcher's flag scan never tracked which tokens it had already consumed
|
|
18
|
+
as another flag's value, so it re-inspected a value token as if it were
|
|
19
|
+
fresh input, misreading a flag's value as the subcommand name in the first
|
|
20
|
+
case and re-scanning a dash-prefixed value as its own (unknown) flag in the
|
|
21
|
+
second. Fixing the scan surfaced a second, more serious defect on the same
|
|
22
|
+
path: once a leading global flag was correctly found, the token AFTER it
|
|
23
|
+
was dropped entirely on the way to the resolved command, so the flag's
|
|
24
|
+
value silently never reached the request at all (a stored profile or
|
|
25
|
+
environment variable answered instead, with no error). All three are
|
|
26
|
+
fixed. Global flags now work in any position, and a flag value starting
|
|
27
|
+
with `-` (an API key, a negative `--limit`, a message beginning with a
|
|
28
|
+
dash) is bound exactly as typed.
|
|
29
|
+
|
|
30
|
+
## [0.23.0] - 2026-08-10
|
|
31
|
+
|
|
32
|
+
A security and correctness release. Upgrade promptly if `--account` is ever
|
|
33
|
+
set from anything other than a literal you typed yourself (an environment
|
|
34
|
+
variable, a config profile, or agent- or model-generated text): every
|
|
35
|
+
published version through 0.22.0 interpolated it into the request path with
|
|
36
|
+
no validation at all, so a value carrying a slash, `..`, a question mark, a
|
|
37
|
+
hash, or a percent sign could redirect a request, including a write, to a
|
|
38
|
+
different endpoint on the API host. This release also lets `--account` take a
|
|
39
|
+
connected account's name, not just its id.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **`--account` could redirect a request to a different endpoint.**
|
|
44
|
+
`inbox mark-read` on `--account 'x/../../../v1/accounts'` built
|
|
45
|
+
`PATCH /v1/accounts/chats/chat_1` instead of touching the account the caller
|
|
46
|
+
named. `--account 'a?x=1'` injected a query string into the middle of the
|
|
47
|
+
path. Neither needed a literal `..`: the URL Standard percent-decodes when it
|
|
48
|
+
decides whether a segment is a double-dot path segment, so `%2e%2e` walked up
|
|
49
|
+
the path with no slash in the value at all. Every account-scoped command was
|
|
50
|
+
reachable this way, since all of them build the request URL from `--account`.
|
|
51
|
+
The value is now checked before any request is built, and a value that could
|
|
52
|
+
redirect one is refused with `[INVALID_PATH_SEGMENT]` (exit `2`) naming the
|
|
53
|
+
character it contains, rather than being sent.
|
|
54
|
+
|
|
55
|
+
- **`group get` / `group members` never actually accepted a group URL,
|
|
56
|
+
despite documenting it.** The help and this file both claimed the server
|
|
57
|
+
extracted the numeric id from a full `https://www.linkedin.com/groups/...`
|
|
58
|
+
URL passed through verbatim; it does not, and cannot: the URL's own slashes
|
|
59
|
+
split it into several path segments and the request landed on a route that
|
|
60
|
+
does not exist. The numeric id is now extracted client-side, the same way
|
|
61
|
+
member, company, chat, and job URLs already are. A bare numeric id is
|
|
62
|
+
unaffected.
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- **`--account` accepts a connected account's name, not just its id.** A value
|
|
67
|
+
that is not shaped like an account id is looked up against
|
|
68
|
+
`accounts.list`: an exact name match wins outright, otherwise a unique
|
|
69
|
+
prefix match resolves. A name matching more than one connected account
|
|
70
|
+
exits with `[ACCOUNT_AMBIGUOUS]` (exit `2`) rather than guessing, since
|
|
71
|
+
acting on the wrong live persona cannot be undone. A name matching none
|
|
72
|
+
exits with `[ACCOUNT_NAME_NOT_FOUND]` (exit `4`) and lists what is
|
|
73
|
+
connected. On an API key with more connected accounts than the resolver
|
|
74
|
+
reads (250 per page, 10 pages), a name lookup exits with
|
|
75
|
+
`[ACCOUNT_LIST_TRUNCATED]` (exit `2`) instead of matching against a list
|
|
76
|
+
that might be missing the very account the name would have matched; pass
|
|
77
|
+
the id instead, which needs no lookup. An id-shaped value still costs no
|
|
78
|
+
extra request. `--preview` never issues the lookup, consistent with it
|
|
79
|
+
never calling the API.
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- **`@curviate/sdk` dependency bumped to `^0.20.1`.** ^0.20.0 still resolves
|
|
84
|
+
the vulnerable 0.20.0 build on a fresh install or an old lockfile; 0.20.1 is
|
|
85
|
+
a hard floor at the fix. Percent-encoding of every other command's path
|
|
86
|
+
parameters (chat, job, group, company and member ids, and so on) is the
|
|
87
|
+
SDK's job as of 0.20.0, not this CLI's: a previous CLI-side guard that
|
|
88
|
+
inspected each call's leading string argument has been removed, because it
|
|
89
|
+
could not know which argument actually became a path segment and was wrong
|
|
90
|
+
in both directions, missing a path parameter passed inside an object and
|
|
91
|
+
rejecting a body field that never reached a path (`post save <share URL>`
|
|
92
|
+
was one such false rejection). `--account` is guarded separately, above,
|
|
93
|
+
because it is the one value this CLI genuinely understands the meaning of.
|
|
10
94
|
|
|
11
95
|
## [0.22.0] - 2026-08-07
|
|
12
96
|
|
package/README.md
CHANGED
|
@@ -47,12 +47,14 @@ curviate --api-key <your-api-key> account list
|
|
|
47
47
|
> history. Prefer `curviate login` or the `CURVIATE_API_KEY` environment
|
|
48
48
|
> variable; reserve `--api-key` for one-off, low-trust contexts.
|
|
49
49
|
|
|
50
|
-
Get your API key from the [Curviate dashboard](https://
|
|
50
|
+
Get your API key from the [Curviate dashboard](https://app.curviate.com).
|
|
51
51
|
|
|
52
52
|
## Usage
|
|
53
53
|
|
|
54
54
|
```
|
|
55
|
-
curviate [command] [subcommand] [flags]
|
|
55
|
+
curviate [flags] [command] [subcommand] [flags]
|
|
56
|
+
|
|
57
|
+
Global flags may appear anywhere in the invocation, before the command or after it.
|
|
56
58
|
|
|
57
59
|
Global flags available on every command:
|
|
58
60
|
--account Target a specific account ID
|
|
@@ -62,9 +64,11 @@ Global flags available on every command:
|
|
|
62
64
|
--fields Comma-separated list of fields to include in JSON output
|
|
63
65
|
--limit Maximum number of results to return per page
|
|
64
66
|
--cursor Pagination cursor from a previous response
|
|
65
|
-
--all
|
|
67
|
+
--all Stream all pages as NDJSON
|
|
66
68
|
--max-pages Cap on the number of pages fetched with --all
|
|
69
|
+
--page-delay Milliseconds to pause between pages when --all is used (default 400)
|
|
67
70
|
--preview Show what would happen without sending any write request
|
|
71
|
+
--verbose Output the full SDK response instead of the slim default
|
|
68
72
|
--base-url Override the API base URL (for testing)
|
|
69
73
|
--timeout Request timeout in milliseconds (default: 30000)
|
|
70
74
|
```
|
|
@@ -77,44 +81,59 @@ These examples show how coding agents compose the CLI in real workflows.
|
|
|
77
81
|
|
|
78
82
|
### 1. Find people and send connection requests
|
|
79
83
|
|
|
80
|
-
Search for matching profiles, preview the invitations, then send them once satisfied
|
|
84
|
+
Search for matching profiles, preview the invitations, then send them once satisfied.
|
|
85
|
+
`--location` on `search people` takes location **ids**, not free text; resolve a
|
|
86
|
+
human-readable place name to an id first with `curviate search parameters --type LOCATION`:
|
|
81
87
|
|
|
82
88
|
```bash
|
|
83
|
-
|
|
89
|
+
curviate search parameters --type LOCATION --keywords "Berlin" --account acc_1 --json
|
|
90
|
+
# {"items":[{"id":"103035651","name":"Berlin, Germany"}, {"id":"106967730","name":"Berlin, Berlin, Germany"}, ...]}
|
|
91
|
+
|
|
92
|
+
# Look first, a search is a read, so it just runs (no --preview on reads)
|
|
84
93
|
curviate search people \
|
|
85
94
|
--keywords "AI engineer" \
|
|
86
|
-
--location
|
|
95
|
+
--location 103035651 \
|
|
96
|
+
--account acc_1 \
|
|
87
97
|
--limit 10 \
|
|
88
98
|
--json
|
|
89
99
|
|
|
90
100
|
# Preview a single write before sending, then pipe IDs into connect, one request per person
|
|
91
|
-
curviate connect "$SOME_ID" --note "Hi, I'd love to connect." --preview
|
|
101
|
+
curviate connect "$SOME_ID" --account acc_1 --note "Hi, I'd love to connect." --preview
|
|
92
102
|
|
|
93
|
-
curviate search people --keywords "AI engineer" --location
|
|
103
|
+
curviate search people --keywords "AI engineer" --location 103035651 --account acc_1 --all \
|
|
94
104
|
| jq -r '.id' \
|
|
95
105
|
| head -5 \
|
|
96
|
-
| xargs -I{} curviate connect {} --note "Hi, I'd love to connect."
|
|
106
|
+
| xargs -I{} curviate connect {} --account acc_1 --note "Hi, I'd love to connect."
|
|
97
107
|
```
|
|
98
108
|
|
|
99
109
|
### 2. Triage the inbox and extract unread threads
|
|
100
110
|
|
|
101
|
-
Pull the inbox
|
|
111
|
+
Pull the inbox, filter unread chats, and surface the most recent message from each.
|
|
112
|
+
`--all` streams **NDJSON** (one JSON object per line), not a `{items:[...]}` envelope or a
|
|
113
|
+
bare array, so pipe each line straight into `jq` with no `.[]`. The chat's own id is `id`
|
|
114
|
+
(`chat_id` only appears nested inside `last_message`, pointing back at its own chat); the
|
|
115
|
+
unread signal is the integer `unread_count`, not a boolean `unread`; and `last_message` has
|
|
116
|
+
no `sender` object, only `sender_id` (a provider id, not a display name), so a
|
|
117
|
+
human-readable sender comes from the chat's own `user.display_name` instead (present on
|
|
118
|
+
1:1 chats):
|
|
102
119
|
|
|
103
120
|
```bash
|
|
104
|
-
curviate inbox list --json --all \
|
|
105
|
-
| jq '
|
|
121
|
+
curviate inbox list --json --all --account acc_1 \
|
|
122
|
+
| jq -c 'select(.unread_count > 0) | {chat_id: .id, sender: (.user.display_name // .last_message.sender_id), preview: .last_message.text[0:80]}'
|
|
106
123
|
```
|
|
107
124
|
|
|
108
125
|
### 3. Warm up a prospect by reacting to their recent posts
|
|
109
126
|
|
|
110
|
-
Read recent posts from a profile, then react to each,
|
|
127
|
+
Read recent posts from a profile, then react to each, useful for ambient warm-up before outreach.
|
|
128
|
+
A post's id is `id` (there is no `post_id` field), and `--posts` returns a `{items:[...]}`
|
|
129
|
+
envelope, not a bare array:
|
|
111
130
|
|
|
112
131
|
```bash
|
|
113
132
|
PROFILE_URL="https://www.linkedin.com/in/example"
|
|
114
133
|
|
|
115
|
-
curviate profile "$PROFILE_URL" --posts --fields
|
|
116
|
-
| jq -r '.[].
|
|
117
|
-
| xargs -I{} curviate post react {} --reaction like
|
|
134
|
+
curviate profile "$PROFILE_URL" --posts --fields id --account acc_1 --json \
|
|
135
|
+
| jq -r '.items[].id' \
|
|
136
|
+
| xargs -I{} curviate post react {} --account acc_1 --reaction like
|
|
118
137
|
```
|
|
119
138
|
|
|
120
139
|
### 4. Check tier entitlement before a Sales Navigator sweep
|
|
@@ -122,7 +141,7 @@ curviate profile "$PROFILE_URL" --posts --fields post_id --json \
|
|
|
122
141
|
Exit code `5` means the account lacks the required add-on. Branch on it in a script:
|
|
123
142
|
|
|
124
143
|
```bash
|
|
125
|
-
curviate sales-nav search people --keywords "VP Engineering" --json \
|
|
144
|
+
curviate sales-nav search people --keywords "VP Engineering" --account acc_1 --json \
|
|
126
145
|
|| {
|
|
127
146
|
code=$?
|
|
128
147
|
if [ "$code" -eq 5 ]; then
|
|
@@ -152,11 +171,13 @@ Exit `2` means the signature is invalid or the replay window has expired.
|
|
|
152
171
|
|
|
153
172
|
### 6. Export all accounts to a CSV (agent-friendly pipeline)
|
|
154
173
|
|
|
155
|
-
List every connected account, select key fields, and format as CSV with `jq
|
|
174
|
+
List every connected account, select key fields, and format as CSV with `jq`. `--all` streams
|
|
175
|
+
NDJSON, so slurp it into an array first with `jq -s`; the fields are `account_id` and
|
|
176
|
+
`full_name`, not `id` / `name`:
|
|
156
177
|
|
|
157
178
|
```bash
|
|
158
179
|
curviate account list --all --json \
|
|
159
|
-
| jq -r '["
|
|
180
|
+
| jq -s -r '["account_id","full_name","status"], (.[] | [.account_id, .full_name, .status]) | @csv' \
|
|
160
181
|
> accounts.csv
|
|
161
182
|
```
|
|
162
183
|
|
|
@@ -229,10 +250,13 @@ curviate sales-nav save-lead ACwAAA1234567 --account acc_1 --list 987654
|
|
|
229
250
|
|
|
230
251
|
### 3. Start a new Sales Navigator chat
|
|
231
252
|
|
|
253
|
+
`--subject` is required for Sales Navigator messaging.
|
|
254
|
+
|
|
232
255
|
```bash
|
|
233
256
|
curviate sales-nav message new \
|
|
234
257
|
--to ACwAAA1234567 \
|
|
235
258
|
--account acc_1 \
|
|
259
|
+
--subject "An opportunity at our company" \
|
|
236
260
|
"Hi, I'd love to connect about an opportunity at our company."
|
|
237
261
|
```
|
|
238
262
|
|
|
@@ -314,8 +338,12 @@ curviate recruiter project-job get "$PROJECT_ID" --account acc_1 --json
|
|
|
314
338
|
|
|
315
339
|
### 3. Create a job posting draft, then publish it
|
|
316
340
|
|
|
317
|
-
`recruiter job create` requires `--project-name` (the hiring project the posting opens)
|
|
318
|
-
|
|
341
|
+
`recruiter job create` requires `--project-name` (the hiring project the posting opens) plus the
|
|
342
|
+
full v2 job body: `--job-title`, `--company-id`/`--company-name`, `--workplace-type`, `--location`,
|
|
343
|
+
`--employment-status`, `--seniority-level`, `--description` (200 characters minimum), `--industry`,
|
|
344
|
+
`--job-function`, and `--apply-method`. `--location`/`--industry`/`--job-function` take resolved
|
|
345
|
+
parameter ids, the same `search parameters --type LOCATION`/`--type INDUSTRY`/`--type JOB_FUNCTION`
|
|
346
|
+
resolution from example 1 above. `recruiter job publish` is project-scoped and requires `--mode`
|
|
319
347
|
(`FREE | PROMOTED | PROMOTED_PLUS`); the paid modes also require the full `--budget-*` triple.
|
|
320
348
|
|
|
321
349
|
```bash
|
|
@@ -323,8 +351,15 @@ curviate recruiter job create \
|
|
|
323
351
|
--account acc_1 \
|
|
324
352
|
--project-name "Backend Hiring 2026" \
|
|
325
353
|
--job-title "Senior Backend Engineer" \
|
|
326
|
-
--
|
|
354
|
+
--company-name "Curviate GmbH" \
|
|
355
|
+
--workplace-type REMOTE \
|
|
356
|
+
--location 103035651 \
|
|
327
357
|
--employment-status FULL_TIME \
|
|
358
|
+
--seniority-level MID_SENIOR_LEVEL \
|
|
359
|
+
--description "We are looking for a senior backend engineer to join our remote-first team building the core platform that powers agent-native LinkedIn automation for thousands of developers and their AI agents worldwide." \
|
|
360
|
+
--industry 96 \
|
|
361
|
+
--job-function 15 \
|
|
362
|
+
--apply-method linkedin \
|
|
328
363
|
--json
|
|
329
364
|
|
|
330
365
|
curviate recruiter job publish "$PROJECT_ID" "$JOB_ID" --account acc_1 --mode FREE --json
|
|
@@ -380,7 +415,7 @@ curviate recruiter message new \
|
|
|
380
415
|
--to AEM789 \
|
|
381
416
|
--account acc_1 \
|
|
382
417
|
--subject "A role you'd be a great fit for" \
|
|
383
|
-
--signature "
|
|
418
|
+
--signature "Alex, Talent Team" \
|
|
384
419
|
"Hi, I came across your profile and think you'd be a great fit for a role we're hiring for."
|
|
385
420
|
```
|
|
386
421
|
|
|
@@ -407,6 +442,13 @@ curviate recruiter job get "https://www.linkedin.com/jobs/view/4428113858" --acc
|
|
|
407
442
|
| 3 | Authentication or authorization failure |
|
|
408
443
|
| 4 | Resource not found |
|
|
409
444
|
| 5 | Feature requires an add-on or higher plan |
|
|
445
|
+
| 6 | Rate limited |
|
|
446
|
+
| 7 | Transient platform error (retry likely to succeed) |
|
|
447
|
+
| 8 | Account or connection state blocks the request |
|
|
448
|
+
| 9 | Checkpoint flow error (expired, invalid code, or too many attempts) |
|
|
449
|
+
| 10 | Messaging window expired or recipient unreachable |
|
|
450
|
+
| 11 | Billing issue (payment required, failed, or seat cancelled) |
|
|
451
|
+
| 12 | Auth action needed (a pending checkpoint; not an error) |
|
|
410
452
|
|
|
411
453
|
## License
|
|
412
454
|
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
import {
|
|
3
3
|
readlineSync
|
|
4
4
|
} from "./chunk-H4KV7MIN.js";
|
|
5
|
-
import {
|
|
6
|
-
AUTH_NEEDED
|
|
7
|
-
} from "./chunk-PMRQXBCP.js";
|
|
8
5
|
import {
|
|
9
6
|
slimAccountGet,
|
|
10
7
|
slimAccountList,
|
|
@@ -13,7 +10,7 @@ import {
|
|
|
13
10
|
import {
|
|
14
11
|
pageDelayFromFlags,
|
|
15
12
|
streamAll
|
|
16
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-766W25QS.js";
|
|
17
14
|
import {
|
|
18
15
|
buildPreviewOutput
|
|
19
16
|
} from "./chunk-R3VLWLVV.js";
|
|
@@ -23,15 +20,17 @@ import {
|
|
|
23
20
|
renderSuccess,
|
|
24
21
|
renderUnexpectedError,
|
|
25
22
|
resolveEffectiveConfig
|
|
26
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-TKWW6BV6.js";
|
|
24
|
+
import "./chunk-SYRFLZ4D.js";
|
|
27
25
|
import {
|
|
28
26
|
GLOBAL_FLAGS,
|
|
29
27
|
READ_SINGLE_FLAGS,
|
|
30
|
-
WRITE_SINGLE_FLAGS
|
|
31
|
-
} from "./chunk-CUTMZWL6.js";
|
|
32
|
-
import {
|
|
28
|
+
WRITE_SINGLE_FLAGS,
|
|
33
29
|
defaultReadStdin
|
|
34
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-AHYUM2XQ.js";
|
|
31
|
+
import {
|
|
32
|
+
AUTH_NEEDED
|
|
33
|
+
} from "./chunk-PMRQXBCP.js";
|
|
35
34
|
|
|
36
35
|
// src/commands/account.ts
|
|
37
36
|
import { defineCommand } from "citty";
|
|
@@ -1101,11 +1100,11 @@ var accountCheckpointPollCommand = defineCommand({
|
|
|
1101
1100
|
},
|
|
1102
1101
|
// Override WRITE_SINGLE_FLAGS.timeout: on this command --timeout is the
|
|
1103
1102
|
// --wait loop's own wall-clock bound in MILLISECONDS (requires --wait),
|
|
1104
|
-
// NOT the SDK request timeout
|
|
1105
|
-
//
|
|
1103
|
+
// NOT the SDK request timeout. Default: the time remaining to the
|
|
1104
|
+
// checkpoint's own expiry.
|
|
1106
1105
|
timeout: {
|
|
1107
1106
|
type: "string",
|
|
1108
|
-
description: "Wait-loop timeout in milliseconds (requires --wait; default: time remaining to the checkpoint's expiry).
|
|
1107
|
+
description: "Wait-loop timeout in milliseconds (requires --wait; default: time remaining to the checkpoint's expiry)."
|
|
1109
1108
|
}
|
|
1110
1109
|
},
|
|
1111
1110
|
async run({ args }) {
|
|
@@ -73,11 +73,103 @@ async function resolveTextOrStdin(rawText, out, readStdin) {
|
|
|
73
73
|
return text;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// src/lib/global-flags.ts
|
|
77
|
+
var GLOBAL_FLAGS = {
|
|
78
|
+
// Auth / config
|
|
79
|
+
"api-key": {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "API key (overrides env and profile). Note: a key passed on the command line is visible to other processes via `ps` and is saved in shell history; prefer the CURVIATE_API_KEY env var or `curviate login`."
|
|
82
|
+
},
|
|
83
|
+
profile: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Named profile to use from the config file."
|
|
86
|
+
},
|
|
87
|
+
account: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Account id for account-scoped commands."
|
|
90
|
+
},
|
|
91
|
+
"base-url": {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Override the API base URL."
|
|
94
|
+
},
|
|
95
|
+
timeout: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Request timeout in milliseconds."
|
|
98
|
+
},
|
|
99
|
+
// Output
|
|
100
|
+
json: {
|
|
101
|
+
type: "boolean",
|
|
102
|
+
description: "Emit JSON output (default when stdout is not a TTY).",
|
|
103
|
+
default: false
|
|
104
|
+
},
|
|
105
|
+
fields: {
|
|
106
|
+
type: "string",
|
|
107
|
+
description: "Comma-separated dot-path field projection (e.g. id,name)."
|
|
108
|
+
},
|
|
109
|
+
limit: {
|
|
110
|
+
type: "string",
|
|
111
|
+
description: "Maximum items per page."
|
|
112
|
+
},
|
|
113
|
+
cursor: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "Pagination cursor (opaque token from a previous response)."
|
|
116
|
+
},
|
|
117
|
+
all: {
|
|
118
|
+
type: "boolean",
|
|
119
|
+
description: "Stream all pages as NDJSON.",
|
|
120
|
+
default: false
|
|
121
|
+
},
|
|
122
|
+
"max-pages": {
|
|
123
|
+
type: "string",
|
|
124
|
+
description: "Maximum number of pages to fetch when --all is used."
|
|
125
|
+
},
|
|
126
|
+
"page-delay": {
|
|
127
|
+
type: "string",
|
|
128
|
+
description: "Milliseconds to pause between pages when --all is used (default 400; pass 0 to disable). A modest delay keeps a long stream under the platform rate gate."
|
|
129
|
+
},
|
|
130
|
+
preview: {
|
|
131
|
+
type: "boolean",
|
|
132
|
+
description: "Render the request that would be sent without calling the API.",
|
|
133
|
+
default: false
|
|
134
|
+
},
|
|
135
|
+
verbose: {
|
|
136
|
+
type: "boolean",
|
|
137
|
+
description: "Output the full SDK response instead of the slim default.",
|
|
138
|
+
default: false
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
var WRITE_FLAGS = {
|
|
142
|
+
"api-key": GLOBAL_FLAGS["api-key"],
|
|
143
|
+
profile: GLOBAL_FLAGS.profile,
|
|
144
|
+
account: GLOBAL_FLAGS.account,
|
|
145
|
+
"base-url": GLOBAL_FLAGS["base-url"],
|
|
146
|
+
timeout: GLOBAL_FLAGS.timeout,
|
|
147
|
+
json: GLOBAL_FLAGS.json,
|
|
148
|
+
preview: GLOBAL_FLAGS.preview,
|
|
149
|
+
verbose: GLOBAL_FLAGS.verbose
|
|
150
|
+
};
|
|
151
|
+
var READ_SINGLE_FLAGS = {
|
|
152
|
+
"api-key": GLOBAL_FLAGS["api-key"],
|
|
153
|
+
profile: GLOBAL_FLAGS.profile,
|
|
154
|
+
account: GLOBAL_FLAGS.account,
|
|
155
|
+
"base-url": GLOBAL_FLAGS["base-url"],
|
|
156
|
+
timeout: GLOBAL_FLAGS.timeout,
|
|
157
|
+
json: GLOBAL_FLAGS.json,
|
|
158
|
+
fields: GLOBAL_FLAGS.fields,
|
|
159
|
+
preview: GLOBAL_FLAGS.preview,
|
|
160
|
+
verbose: GLOBAL_FLAGS.verbose
|
|
161
|
+
};
|
|
162
|
+
var WRITE_SINGLE_FLAGS = READ_SINGLE_FLAGS;
|
|
163
|
+
|
|
76
164
|
export {
|
|
77
165
|
STDIN_SENTINEL,
|
|
78
166
|
defaultReadStdin,
|
|
79
167
|
isStdinToken,
|
|
80
168
|
restoreLiteralDashes,
|
|
81
169
|
assertNoStdinPlaceholder,
|
|
82
|
-
resolveTextOrStdin
|
|
170
|
+
resolveTextOrStdin,
|
|
171
|
+
GLOBAL_FLAGS,
|
|
172
|
+
WRITE_FLAGS,
|
|
173
|
+
READ_SINGLE_FLAGS,
|
|
174
|
+
WRITE_SINGLE_FLAGS
|
|
83
175
|
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
assertNoStdinPlaceholder
|
|
4
|
+
} from "./chunk-AHYUM2XQ.js";
|
|
5
|
+
import {
|
|
6
|
+
getExitCode
|
|
7
|
+
} from "./chunk-PMRQXBCP.js";
|
|
8
|
+
|
|
9
|
+
// src/lib/path-safety.ts
|
|
10
|
+
var UNSAFE_CHARS = /[\u0000-\u0020\u007F/\\?#%]/;
|
|
11
|
+
var UNICODE_WHITESPACE = /[\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/;
|
|
12
|
+
function describeChar(value, index) {
|
|
13
|
+
const ch = value[index] ?? "";
|
|
14
|
+
const named = {
|
|
15
|
+
"/": "a slash",
|
|
16
|
+
"\\": "a backslash",
|
|
17
|
+
"?": "a question mark",
|
|
18
|
+
"#": "a hash",
|
|
19
|
+
"%": "a percent sign",
|
|
20
|
+
" ": "a space",
|
|
21
|
+
" ": "a tab",
|
|
22
|
+
"\n": "a newline",
|
|
23
|
+
"\r": "a carriage return"
|
|
24
|
+
};
|
|
25
|
+
return named[ch] ?? `the character U+${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
26
|
+
}
|
|
27
|
+
function firstRedirectingIndex(value) {
|
|
28
|
+
for (let i = 0; i < value.length; i++) {
|
|
29
|
+
const ch = value[i];
|
|
30
|
+
if (ch === " ") continue;
|
|
31
|
+
if (UNSAFE_CHARS.test(ch) || UNICODE_WHITESPACE.test(ch)) return i;
|
|
32
|
+
}
|
|
33
|
+
return -1;
|
|
34
|
+
}
|
|
35
|
+
function redirectingViolation(value) {
|
|
36
|
+
if (value.length === 0) return "it is empty";
|
|
37
|
+
const i = firstRedirectingIndex(value);
|
|
38
|
+
if (i >= 0) return `it contains ${describeChar(value, i)}`;
|
|
39
|
+
if (value.includes("..")) return 'it contains ".."';
|
|
40
|
+
if (value === ".") return 'it is "."';
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function pathSegmentViolation(value) {
|
|
44
|
+
const redirecting = redirectingViolation(value);
|
|
45
|
+
if (redirecting !== null) return redirecting;
|
|
46
|
+
if (value.includes(" ")) return "it contains a space";
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function pathSegmentErrorMessage(label, value, reason) {
|
|
50
|
+
return `error: [INVALID_PATH_SEGMENT] ${label}: ${reason}, so it cannot be part of a request path. A value carrying a slash, backslash, question mark, hash, percent sign, whitespace, or ".." would redirect the request to a different endpoint. Received: ${JSON.stringify(value)}.
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/lib/account-arg.ts
|
|
55
|
+
var ACCOUNT_ID_RE = /^acc_[A-Za-z0-9_-]+$/;
|
|
56
|
+
var listCache = /* @__PURE__ */ new WeakMap();
|
|
57
|
+
var MAX_LOOKUP_PAGES = 10;
|
|
58
|
+
function toConnectedAccount(item) {
|
|
59
|
+
if (item === null || typeof item !== "object") return null;
|
|
60
|
+
const row = item;
|
|
61
|
+
const accountId = row["account_id"];
|
|
62
|
+
if (typeof accountId !== "string" || accountId.length === 0) return null;
|
|
63
|
+
const fullName = row["full_name"];
|
|
64
|
+
return {
|
|
65
|
+
accountId,
|
|
66
|
+
fullName: typeof fullName === "string" && fullName.length > 0 ? fullName : null
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function listConnectedAccounts(client) {
|
|
70
|
+
const cached = listCache.get(client);
|
|
71
|
+
if (cached) return cached;
|
|
72
|
+
const pending = (async () => {
|
|
73
|
+
const accounts = [];
|
|
74
|
+
let cursor;
|
|
75
|
+
for (let page = 0; page < MAX_LOOKUP_PAGES; page++) {
|
|
76
|
+
const params = { limit: 250 };
|
|
77
|
+
if (cursor) params["cursor"] = cursor;
|
|
78
|
+
const result = await client.accounts.list(
|
|
79
|
+
params
|
|
80
|
+
);
|
|
81
|
+
for (const item of result.items ?? []) {
|
|
82
|
+
const account = toConnectedAccount(item);
|
|
83
|
+
if (account) accounts.push(account);
|
|
84
|
+
}
|
|
85
|
+
if (!result.cursor) return { accounts, complete: true };
|
|
86
|
+
cursor = result.cursor;
|
|
87
|
+
}
|
|
88
|
+
return { accounts, complete: false };
|
|
89
|
+
})();
|
|
90
|
+
listCache.set(client, pending);
|
|
91
|
+
return pending;
|
|
92
|
+
}
|
|
93
|
+
function describeAccount(account) {
|
|
94
|
+
return account.fullName === null ? account.accountId : `"${account.fullName}" (${account.accountId})`;
|
|
95
|
+
}
|
|
96
|
+
function describeAll(accounts) {
|
|
97
|
+
return accounts.map(describeAccount).join(", ");
|
|
98
|
+
}
|
|
99
|
+
function matchAccounts(accounts, selector) {
|
|
100
|
+
const needle = selector.toLowerCase();
|
|
101
|
+
const byId = accounts.filter((a) => a.accountId === selector);
|
|
102
|
+
if (byId.length > 0) return byId;
|
|
103
|
+
const exactName = accounts.filter((a) => a.fullName?.toLowerCase() === needle);
|
|
104
|
+
if (exactName.length > 0) return exactName;
|
|
105
|
+
return accounts.filter((a) => a.fullName?.toLowerCase().startsWith(needle));
|
|
106
|
+
}
|
|
107
|
+
async function requireAccount(client, flags, out) {
|
|
108
|
+
const account = flags.account;
|
|
109
|
+
if (!account) {
|
|
110
|
+
out.stderr.write(
|
|
111
|
+
"error: --account is required for this command. Set it via --account, CURVIATE_ACCOUNT, or `curviate config set-account`.\n"
|
|
112
|
+
);
|
|
113
|
+
process.exit(2);
|
|
114
|
+
}
|
|
115
|
+
const selector = account.trim();
|
|
116
|
+
assertNoStdinPlaceholder("the --account value", [selector]);
|
|
117
|
+
const redirecting = redirectingViolation(selector);
|
|
118
|
+
if (redirecting !== null) {
|
|
119
|
+
out.stderr.write(pathSegmentErrorMessage("--account", selector, redirecting));
|
|
120
|
+
process.exit(2);
|
|
121
|
+
}
|
|
122
|
+
if (ACCOUNT_ID_RE.test(selector)) return selector;
|
|
123
|
+
if (flags.preview === true) {
|
|
124
|
+
out.stderr.write(
|
|
125
|
+
`note: --preview does not call the API, so --account "${selector}" is shown as you typed it. A real run resolves it to an account id first, and fails if it matches no connected account or more than one.
|
|
126
|
+
`
|
|
127
|
+
);
|
|
128
|
+
return selector;
|
|
129
|
+
}
|
|
130
|
+
let listed;
|
|
131
|
+
try {
|
|
132
|
+
listed = await listConnectedAccounts(client);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const { CurviateError } = await import("@curviate/sdk");
|
|
135
|
+
if (err instanceof CurviateError) {
|
|
136
|
+
const e = err;
|
|
137
|
+
out.stderr.write(
|
|
138
|
+
`error: [${e.code}] could not look up connected accounts to resolve --account "${selector}": ${e.message}
|
|
139
|
+
`
|
|
140
|
+
);
|
|
141
|
+
process.exit(getExitCode(e.code));
|
|
142
|
+
}
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
if (!listed.complete) {
|
|
146
|
+
out.stderr.write(
|
|
147
|
+
`error: [ACCOUNT_LIST_TRUNCATED] --account "${selector}" cannot be resolved by name: this API key has more connected accounts than the resolver reads (it stops after ${MAX_LOOKUP_PAGES} pages of 250), so the name would be matched against an incomplete list and a single match would not prove there is only one. Pass the account id instead; an id is used as given and needs no lookup.
|
|
148
|
+
`
|
|
149
|
+
);
|
|
150
|
+
process.exit(2);
|
|
151
|
+
}
|
|
152
|
+
const accounts = listed.accounts;
|
|
153
|
+
const matches = matchAccounts(accounts, selector);
|
|
154
|
+
if (matches.length === 1) {
|
|
155
|
+
const resolved = matches[0].accountId;
|
|
156
|
+
const bad = pathSegmentViolation(resolved);
|
|
157
|
+
if (bad !== null) {
|
|
158
|
+
out.stderr.write(
|
|
159
|
+
pathSegmentErrorMessage(`the account id resolved from "${selector}"`, resolved, bad)
|
|
160
|
+
);
|
|
161
|
+
process.exit(2);
|
|
162
|
+
}
|
|
163
|
+
return resolved;
|
|
164
|
+
}
|
|
165
|
+
if (matches.length > 1) {
|
|
166
|
+
out.stderr.write(
|
|
167
|
+
`error: [ACCOUNT_AMBIGUOUS] --account "${selector}" matches ${matches.length} connected accounts: ${describeAll(matches)}. Pass the account id of the one you mean; this is not resolved by guessing, because acting as the wrong account cannot be undone.
|
|
168
|
+
`
|
|
169
|
+
);
|
|
170
|
+
process.exit(2);
|
|
171
|
+
}
|
|
172
|
+
const known = accounts.length === 0 ? "No accounts are connected on this API key." : `Connected accounts: ${describeAll(accounts)}.`;
|
|
173
|
+
out.stderr.write(
|
|
174
|
+
`error: [ACCOUNT_NAME_NOT_FOUND] --account "${selector}" matched no connected account. ${known}
|
|
175
|
+
`
|
|
176
|
+
);
|
|
177
|
+
process.exit(4);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export {
|
|
181
|
+
requireAccount
|
|
182
|
+
};
|