@curviate/cli 0.14.0 → 0.15.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +52 -79
  3. package/dist/{account-7AUDAMIK.js → account-FM473HEK.js} +38 -371
  4. package/dist/{chunk-SSPILTKF.js → chunk-45KHSWCV.js} +56 -48
  5. package/dist/chunk-BGZW6B7G.js +59 -0
  6. package/dist/chunk-EEMJDJ4W.js +80 -0
  7. package/dist/{chunk-JXF47TRY.js → chunk-M33MI53G.js} +33 -1
  8. package/dist/chunk-QJZ3LWOX.js +24 -0
  9. package/dist/{chunk-4JHGVY7R.js → chunk-TMU3CSPR.js} +4 -0
  10. package/dist/{chunk-HMAGEMF7.js → chunk-ZE7QGR3F.js} +4 -0
  11. package/dist/cli.js +127 -15
  12. package/dist/comment-NCDXERSI.js +555 -0
  13. package/dist/{company-RU2CA5DY.js → company-IYTMW64G.js} +27 -74
  14. package/dist/{config-Q2AEWSEC.js → config-P5CPF5WC.js} +1 -1
  15. package/dist/{connect-QT6ZOS75.js → connect-TTJHMBUP.js} +91 -45
  16. package/dist/{exit-codes-ZTY2NY3X.js → exit-codes-SL3GQF7W.js} +1 -1
  17. package/dist/{inbox-DN7X7CM2.js → inbox-EOOGJ3ZN.js} +55 -124
  18. package/dist/job-YARGTHUY.js +631 -0
  19. package/dist/{login-BBVQLXM7.js → login-MFB45PVZ.js} +1 -1
  20. package/dist/{message-6K5E3CUF.js → message-2DLIQIE6.js} +61 -51
  21. package/dist/{post-2JQZ3OSF.js → post-GRIJ7X52.js} +188 -221
  22. package/dist/profile-57RJEL7H.js +809 -0
  23. package/dist/{recruiter-XHVMQWK6.js → recruiter-CTYH7AYG.js} +740 -226
  24. package/dist/{sales-nav-QTSTJMKA.js → sales-nav-NUMEYOEO.js} +119 -77
  25. package/dist/{search-ZGTS45BT.js → search-RD4TXNV7.js} +103 -39
  26. package/dist/{webhook-EIY5WWTE.js → webhook-CEP7SGP5.js} +8 -45
  27. package/package.json +4 -3
  28. package/dist/chunk-GXTZION6.js +0 -45
  29. package/dist/chunk-Q43HZUN3.js +0 -29
  30. package/dist/job-FGGQEXSU.js +0 -105
  31. package/dist/profile-DD5GMGNL.js +0 -501
package/CHANGELOG.md CHANGED
@@ -6,6 +6,167 @@ 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
+ ## [0.15.1] - 2026-07-11
10
+
11
+ A patch release of agent-experience (AX) and developer-experience (DX)
12
+ improvements: clearer errors and help, a modest default pacing on `--all`
13
+ streams, one back-compatible reaction-signature unification, and a
14
+ `profile endorse` fix. No breaking changes.
15
+
16
+ ### Added
17
+
18
+ - **Successor hints for removed/renamed commands.** Reaching for a command that
19
+ moved or was removed in 0.15.0 — `post list`, `post comment`/`comments`,
20
+ `connect respond`, `profile connections`, `account connect-link`/`reconnect-link`/`reconnect`,
21
+ `inbox sync`/`sync-chat`, `recruiter add-candidate`/`project-jobs`/`sync`,
22
+ `sales-nav sync`, `webhook state-diff`, `company followers` — now prints a
23
+ one-line "did you mean" pointer to the replacement instead of a bare
24
+ "unknown command". The exit code is unchanged (2).
25
+ - **`--all` NDJSON-mode notice.** When `--all` streaming engages, a one-line
26
+ notice on stderr makes the format switch explicit (`--all` streams NDJSON —
27
+ one object per line — not the `{items, cursor}` envelope), so an agent
28
+ pattern-matching the plain-mode shape does not mis-parse the stream.
29
+ - **`--page-delay <ms>` and default `--all` pacing.** `--all` now pauses a
30
+ modest default between page fetches, keeping a long stream under the platform
31
+ rate gate. `--page-delay <ms>` overrides it (pass `0` to disable).
32
+ - **`job list --state ALL`.** A best-effort client-side union across every state
33
+ (DRAFT/OPEN/CLOSED/REVIEW/SUSPENDED): each state is queried, re-filtered
34
+ against its own state, then merged and de-duplicated by id. There is no
35
+ unified cursor — each state is walked independently and `--max-pages` applies
36
+ per state.
37
+ - **`--fields` unknown-field warning.** Projecting a field that matches nothing
38
+ on the response now emits one stderr warning naming the unmatched fields and
39
+ listing the available keys, instead of silently returning `{}`. The output is
40
+ unchanged — the known fields still project.
41
+
42
+ ### Changed
43
+
44
+ - **Reaction commands unified on the positional form.** `post react <post_id>
45
+ <reaction>` and `message react <chat_id> <message_id> <emoji>` now take the
46
+ reaction/emoji as a positional argument, matching `comment react`/`unreact`
47
+ and `post unreact`. The previous `--reaction` and `--emoji` flags still work as
48
+ deprecated aliases (no breaking removal). A missing value is now a usage error
49
+ (exit 2) rather than a silent empty reaction.
50
+ - **Constraint discoverability in help.** `job create`/`job update` help now
51
+ states the 200-character minimum on `--description` explicitly, and
52
+ `job publish --budget-amount` notes it must be non-negative.
53
+ - **List-lag notes.** `post user-posts`, `comment list`, `inbox messages`, and
54
+ `connect sent`/`received` help now note that a very recent create/delete may
55
+ take a few minutes to appear or clear (LinkedIn-side indexing), and that a
56
+ direct `get` reflects a change immediately.
57
+
58
+ ### Fixed
59
+
60
+ - **`profile endorse <slug|url>`** now resolves the handle to the member's
61
+ provider id before endorsing (via a contact-safe profile read), matching
62
+ `profile follow`/`unfollow`. Previously a slug or URL 404'd because the
63
+ endorse endpoint accepts only the provider id; the provider-id form was
64
+ unaffected.
65
+
66
+ ## [0.15.0] - 2026-07-11
67
+
68
+ Full v2 API-surface parity — the coupled release with `@curviate/sdk` 0.15.0. A large
69
+ **breaking** minor (pre-1.0): the CLI is re-pointed onto the v2-only API, drops the
70
+ commands whose endpoint no longer exists, relocates several verbs, and adds commands for
71
+ the new v2 methods. Every command noun kept its intent-shaped name; only the wiring,
72
+ removed orphans, relocations, and additions changed.
73
+
74
+ ### Removed (BREAKING)
75
+
76
+ Commands whose underlying v2 endpoint no longer exists:
77
+
78
+ - **`account connect-link`**, **`account reconnect-link`**, **`account reconnect`** — the hosted-link and in-place re-auth flows. Connect a new account with `account link`; poll a hosted session with `account connect-session poll`.
79
+ - **`company followers`**
80
+ - **`inbox sync`**, **`inbox sync-chat`** — message history now syncs implicitly.
81
+ - **`post list`**
82
+ - **`recruiter sync`**, **`recruiter add-applicant`**, **`recruiter reject-applicant`**, **`recruiter job checkpoint`**
83
+ - **`sales-nav sync`**
84
+ - **`webhook state-diff`**
85
+
86
+ Flags with no v2 request-side home — dropped entirely (not defined, parsed, or forwarded), with no replacement:
87
+
88
+ - **`profile --notify`** — signal-a-view has no v2 request field.
89
+ - **`message inmail --surface`** — the v2 send-InMail body carries no surface/type discriminator.
90
+ - **`post create --video-thumbnail`** — v2 posts carry media only via `--attach`.
91
+ - **`search people|companies|posts|jobs --url`** — from-URL search is now the bare `search <url>` form.
92
+
93
+ ### Changed (BREAKING)
94
+
95
+ Renames and relocations:
96
+
97
+ - **`post comment`** / **`post comments`** (and `post react --comment-id`) → the new **`comment`** group (`comment add`, `comment reply`, `comment react`, and the rest). Comment threads are first-class.
98
+ - **`connect respond --accept` / `--decline`** → **`connect accept <id>`** / **`connect decline <id>`**; the combined `respond` is removed.
99
+ - **`recruiter add-candidate`** → **`recruiter save-candidate <project_id> --stage-id <id> --candidate-id <id>`** (full body reshape).
100
+ - **`recruiter project-jobs`** → **`recruiter project-job get <project_id>`** (cardinality fix — a project has at most one attached posting; single-object read, no pagination).
101
+ - **`recruiter job applicants`** → **`recruiter applicants <project_id>`** (the applicant list is project-scoped, not job-scoped; `--channel-id` still required).
102
+ - **`profile connections`** → **`profile relations`**.
103
+ - **`profile endorse --skill`** → **`profile endorse --endorsement-id`** — value semantics unchanged (still the target's `endorsement_id`, obtained from their skills section via `profile <id> --sections skills`); the old flag name misleadingly suggested a skill name.
104
+
105
+ CLI-visible shape changes:
106
+
107
+ - **`job publish`** now requires **`--mode`** (`FREE | PROMOTED | PROMOTED_PLUS`); `PROMOTED`/`PROMOTED_PLUS` additionally require the full `--budget-*` triple.
108
+ - **`recruiter job create`** now requires **`--project-name`** and takes the full v2 job body — `--employment-status` replaces the pre-v2 `--employment-type` on this command, alongside the company / workplace / location flags.
109
+ - **`recruiter message new`** is now **JSON-only** (file/voice/video attachments ride the body as base64 — no multipart) and requires **`--subject`** and **`--signature`**.
110
+
111
+ Dependency and request grammar:
112
+
113
+ - **`@curviate/sdk` bumped to `0.15.0`.**
114
+ - **Account-first path grammar.** Every account-scoped request now addresses the account in the URL path instead of a query/body field. This is handled entirely inside the SDK — no CLI syntax changes — but every command's underlying request moved.
115
+
116
+ ### Added
117
+
118
+ New **`comment`** command group (the comment-thread surface):
119
+
120
+ - `comment list <post_id>`, `comment add`, `comment reply`, `comment edit`, `comment delete`, `comment replies`, `comment react`, `comment reactions`, `comment unreact`, `comment user`.
121
+
122
+ Job-posting management — the **`job`** family:
123
+
124
+ - `job list`, `job create`, `job update`, `job budget`, `job publish`, `job close`, `job applicants`, `job applicant get`, and `job applicant resume` (binary résumé download via `-o`).
125
+
126
+ Profile:
127
+
128
+ - `profile update`, `profile follow`, `profile unfollow`, `profile following` (alongside `profile followers`).
129
+
130
+ Posts:
131
+
132
+ - `post delete`, `post unreact`, `post user-posts`, `post user-reactions`.
133
+
134
+ Search and inbox:
135
+
136
+ - `search <url>` (run a pasted search / saved-search / lead-list URL directly), `inbox mark-read`.
137
+
138
+ Recruiter (project-centric surface):
139
+
140
+ - `recruiter projects`, `recruiter project`, `recruiter project update`, `recruiter pipeline`, `recruiter project-job get|create|budget|update`, `recruiter talent-search`, `recruiter save-candidate`, `recruiter applicants`, `recruiter applicant get|resume`, plus `recruiter job close` and `recruiter search <url>`.
141
+
142
+ Sales Navigator:
143
+
144
+ - `sales-nav search <url>`, plus the v2 list surface: `sales-nav account-lists`, `lead-lists`, `browse-account-list`, `browse-lead-list`, `save-account`.
145
+
146
+ Account:
147
+
148
+ - **`account link --account-id <acc_…>`** (optional, non-breaking) — re-authenticate an existing account **in place** (reconnect): passing the id makes `account link` an in-place reconnect of that account; omit it for an ordinary fresh connect (unchanged — no `account_id` is sent). This is the reconnect path now that the hosted `account reconnect` / `account reconnect-link` commands are removed.
149
+
150
+ Exit-code mapping:
151
+
152
+ - **`ACCOUNT_ALREADY_LINKED`** and **`LINKEDIN_OPERATION_NOT_SUPPORTED`** are now present in `EXIT_CODE_MAP` (exit `8`, account / connection state — grouped with `ACCOUNT_RESTRICTED`/`RESOURCE_ACCESS_RESTRICTED`); previously `ACCOUNT_ALREADY_LINKED` was already a valid SDK `ErrorCode` but had no exit-code entry, and `LINKEDIN_OPERATION_NOT_SUPPORTED` is a new SDK code (a permanent LinkedIn platform limitation for the attempted operation, e.g. listing a non-self user's following list). Both were silently falling through to the default exit `1`; the exhaustiveness test now covers them.
153
+ - **`CONNECTION_REQUEST_CONFLICT`** (exit `8`, account / connection state — the documented "already invited or already connected" contract on a `connect` retry) and **`RATE_LIMITED`** (exit `6`, rate-limited — a general/unscoped rate-limit signal alongside `RATE_LIMIT_ACCOUNT`/`RATE_LIMIT_TENANT`/`PLATFORM_RATE_LIMIT`/`LINKEDIN_RATE_LIMITED`) are new SDK error codes, both now present in `EXIT_CODE_MAP`. Picked up via the refreshed `@curviate/sdk` 0.15.0 tarball dependency (pnpm-lock.yaml integrity hash only).
154
+
155
+ ### Fixed
156
+
157
+ - **`company <id> employees` / `company <id> posts` / `company <id> jobs` (id-first form) no longer silently returns the base company profile.** The router bound `<id>` and dropped the trailing sub-resource word, so the id-first form quietly returned the company profile with exit 0. It now routes the id-first form to the sub-resource (equivalent to `company <sub> <id>`), or exits 2 with an actionable error on a genuinely unexpected extra argument — never a silent wrong result. The guard is applied uniformly across every bare-form command group.
158
+ - **`company employees|posts|jobs <slug>` (or a company URL) now works.** The three sub-resources previously required the numeric company id and erred on a handle; they now auto-resolve a slug/URL to the numeric id the same way the bare `company <slug>` retrieve does (a numeric id still passes straight through; a genuinely unresolvable identifier surfaces the not-found error).
159
+ - **`profile follow <slug>` / `profile unfollow <slug>` (or a member URL) now work.** The follow endpoint accepts only a provider id, so a slug returned "not found"; both commands now resolve the identifier to the member's provider id first — the same auto-resolution `profile`, `connect`, and `message` already do.
160
+ - **`company posts` / `search posts` slim `--json` output no longer emits a permanently-null `post_urn`/`posted_at` and silently drops the post's own id (D13).** Both endpoints share the identical v2 item schema (`{id, share_url, text, author, reaction_count, comment_count, repost_count, is_repost, attachments, reactions, permissions}`) — `post_urn` was never a real key and `posted_at` doesn't exist on this resource at all. Slim output now surfaces the real `id`; `--fields` projects the real v2 keys. `share_url`/`repost_count`/`is_repost`/`attachments`/`reactions`/`permissions`/the full `author` object remain verbose-only.
161
+ - **`company <id>` slim `--json` output no longer emits permanently-null `employee_count`/`employee_count_range`/`followers_count`/`foundation_date`, and drops the entirely-fictitious `messaging` field.** The real v2 company-profile response nests headcount data at `insights.headcount` / `insights.headcount_range.from` (the range has no upper bound at all — documented open-ended-high, so no `to` is invented), the establishment date is a bare year at `establishment_year` (not a date string), the follower count key is singular, and there is no `messaging` field anywhere on this resource. Slim output now surfaces real `employee_count` / `employee_count_range` (`{from}` only) values; **`foundation_date` is renamed `establishment_year`** and **`followers_count` is renamed `follower_count`** (both now real, non-null); `messaging` is removed outright. `headquarters` (synthesized from `locations`) now reads the real `country_code`/`postal_code` location keys — the fictitious `country` key never existed and always projected null, and is renamed `country_code`; `postal_code` is added. `area` (region/state, e.g. "Washington") stays — it's real and often populated (verified live), even though the SDK's OpenAPI-generated types don't declare it for this endpoint.
162
+ - **`job get` / `recruiter job get` slim `--json` output no longer emits a permanently-null `company_id`/`applicants_counter`, and `job get` (Core) no longer emits a permanently-null `published_at`.** `company_id` is synthesized from the nested `company.id` (neither shape has a top-level `company_id`); `applicants_counter` is renamed to `applications_count` (the real key on both shapes); `published_at` falls back to `created_at` on the Core shape, which has no `published_at` field at all (the Recruiter shape's own `published_at` is real and unaffected).
163
+ - **`profile me` / `profile <id>` slim `--json` output no longer emits permanently-null `provider_id`/`network_distance`/`is_premium`/`current_position`, restores `headline` sourced from its real location, and drops the entirely-fictitious `occupation`/`organizations` fields.** Both commands are backed by the identical real v2 user-profile response — there is no top-level `provider_id` (the real identifier is `id`), no top-level `network_distance`/`is_premium` (both nested under `specifics`), and no top-level `work_experience` (the real array is `specifics.experience`, which fed the `current_position` synthesis — also permanently null until now). `provider_id` now sources from `id`; `network_distance` and `is_premium` now source from `specifics.network_distance`/`specifics.is_premium`; `current_position` is synthesized from `specifics.experience[0]`. **`profile me`'s `email` is renamed `emails`** (the real field is a plural array, not a singular string). **`headline` ← `description`** — on a v2 read, LinkedIn serves the profile headline in the `description` wire field (a separate `bio` field carries the About-section paragraph); initially assumed to have no v2 source and dropped, then restored once the real source was confirmed live (3-way evidence: a written headline read back via `description` byte-for-byte, the same result from the M3 matrix probe, and `--verbose` showing headline-shaped text in `description` across live profiles). `occupation` and `organizations` are removed outright — neither has a v2 source; the real user-profile response has no occupation-summary field and no administered-organizations field of any kind. (`profile me`'s slim output drops from 10 fields to 9; `profile <id>`'s drops from 9 to 8.)
164
+ - **`job list --state` now re-filters returned items against their own `state` (D10).** LinkedIn's upstream state filter is best-effort — it commonly returns items whose own `state` doesn't match the request. `--json` output (and `--all` streaming) now only contains items whose own `state` actually matches (`--state OPEN` maps to the response's `LISTED`, the one value that differs between the request and response vocabularies); dropped items produce a stderr note with the count. The re-filter is page-local and never touches the pagination cursor, so `--all` still walks the same unfiltered upstream pages — it may fetch more pages than the filtered item count implies. The `--state` help text now says so.
165
+
166
+ ### Notes — no user action required
167
+
168
+ - **`post react --as-organization`**: unchanged at the flag level; only the internal wire key was renamed, so the flag behaves exactly as before.
169
+
9
170
  ## [0.14.0] - 2026-07-07
10
171
 
11
172
  Webhooks surface cascade — the coupled release with `@curviate/sdk` 0.14.0. Additive minor.
package/README.md CHANGED
@@ -80,14 +80,16 @@ These examples show how coding agents compose the CLI in real workflows.
80
80
  Search for matching profiles, preview the invitations, then send them once satisfied:
81
81
 
82
82
  ```bash
83
- # Preview first — see who would be targeted
83
+ # Look first — a search is a read, so it just runs (no --preview on reads)
84
84
  curviate search people \
85
85
  --keywords "AI engineer" \
86
86
  --location "Berlin" \
87
87
  --limit 10 \
88
- --preview
88
+ --json
89
+
90
+ # 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
89
92
 
90
- # Pipe IDs into connect — one request per person
91
93
  curviate search people --keywords "AI engineer" --location "Berlin" --all \
92
94
  | jq -r '.id' \
93
95
  | head -5 \
@@ -112,7 +114,7 @@ PROFILE_URL="https://www.linkedin.com/in/example"
112
114
 
113
115
  curviate profile "$PROFILE_URL" --posts --fields post_id --json \
114
116
  | jq -r '.[].post_id' \
115
- | xargs -I{} curviate post react {} --type LIKE
117
+ | xargs -I{} curviate post react {} --reaction like
116
118
  ```
117
119
 
118
120
  ### 4. Check tier entitlement before a Sales Navigator sweep
@@ -193,16 +195,6 @@ curviate company posts 112013061 --limit 5 --account acc_1 --json
193
195
  curviate company jobs 112013061 --all --account acc_1 --json # streams every page
194
196
  ```
195
197
 
196
- ### 3. List a company's followers (page admins only)
197
-
198
- Followers are only retrievable for a company page the acting account **administers** — a
199
- non-administered company returns a `RESOURCE_NOT_FOUND` error (exit code `4`), the same shape as
200
- an unknown company.
201
-
202
- ```bash
203
- curviate company followers 112013061 --limit 25 --account acc_1 --json
204
- ```
205
-
206
198
  ## Sales Navigator
207
199
 
208
200
  Sales Navigator commands (`curviate sales-nav ...`) require an account with the Sales Navigator
@@ -297,9 +289,10 @@ curviate sales-nav save-account 112013061 --account acc_1 --list 987654
297
289
 
298
290
  Recruiter commands (`curviate recruiter ...`) require an account with the Recruiter add-on tier
299
291
  attached. A call against an account without it fails with **exit code `5`** and a `TIER_NOT_ACTIVE`
300
- error body naming the required tier (`recruiter`). Write commands (`add-candidate`,
301
- `add-applicant`, `reject-applicant`, `job create`/`publish`/`checkpoint`, `message new`) accept
302
- `--preview` to render the request without sending it.
292
+ error body naming the required tier (`recruiter`). The surface is project-centric: most
293
+ operations are scoped to a hiring project id. Write commands (`save-candidate`, `project update`,
294
+ `project-job create`/`update`, `job create`/`publish`/`close`, `message new`) accept `--preview`
295
+ to render the request without sending it.
303
296
 
304
297
  ### 1. List hiring projects
305
298
 
@@ -308,65 +301,62 @@ curviate recruiter projects --account acc_1 --limit 20 --json \
308
301
  | jq -r '.items[] | "\(.id)\t\(.name)"'
309
302
  ```
310
303
 
311
- ### 2. Create a job posting draft, then publish it (with the checkpoint flow)
304
+ ### 2. Inspect a project, its pipeline, and its attached job posting
305
+
306
+ `recruiter project-job get` returns the single job posting attached to a project (a
307
+ `RESOURCE_NOT_FOUND` / exit `4` when none is attached).
308
+
309
+ ```bash
310
+ curviate recruiter project "$PROJECT_ID" --account acc_1 --json
311
+ curviate recruiter pipeline "$PROJECT_ID" --account acc_1 --json
312
+ curviate recruiter project-job get "$PROJECT_ID" --account acc_1 --json
313
+ ```
314
+
315
+ ### 3. Create a job posting draft, then publish it
312
316
 
313
- Publishing can return a verification checkpoint instead of a published job solve it with the
314
- `job_id` from the publish response, then retry:
317
+ `recruiter job create` requires `--project-name` (the hiring project the posting opens) and takes
318
+ the v2 job body. `recruiter job publish` is project-scoped and requires `--mode`
319
+ (`FREE | PROMOTED | PROMOTED_PLUS`); the paid modes also require the full `--budget-*` triple.
315
320
 
316
321
  ```bash
317
322
  curviate recruiter job create \
318
323
  --account acc_1 \
324
+ --project-name "Backend Hiring — 2026" \
319
325
  --job-title "Senior Backend Engineer" \
320
326
  --description "Remote-first team building the core platform." \
321
- --employment-type FULL_TIME \
322
- --json > draft.json
323
-
324
- JOB_ID=$(jq -r '.job_id' draft.json)
327
+ --employment-status FULL_TIME \
328
+ --json
325
329
 
326
- curviate recruiter job publish "$JOB_ID" --account acc_1 --mode FREE --json > publish.json
327
-
328
- # If publish returns a checkpoint object instead of a published job, solve it:
329
- if [ "$(jq -r '.object // empty' publish.json)" = "job_posting_checkpoint" ]; then
330
- curviate recruiter job checkpoint "$JOB_ID" --account acc_1 --input "123456"
331
- fi
330
+ curviate recruiter job publish "$PROJECT_ID" "$JOB_ID" --account acc_1 --mode FREE --json
332
331
  ```
333
332
 
334
- ### 3. List applicants for a job, then get one applicant's detail
333
+ ### 4. List applicants in a project, then get one applicant's detail
334
+
335
+ `recruiter applicants` is project-scoped and requires `--channel-id` (the project's own
336
+ JOB_POSTING talent-pool channel). Applicant detail and résumé are also project-scoped.
335
337
 
336
338
  ```bash
337
- curviate recruiter job applicants "$JOB_ID" --account acc_1 --limit 10 --json \
339
+ curviate recruiter applicants "$PROJECT_ID" --channel-id "$CHANNEL_ID" --account acc_1 --limit 10 --json \
338
340
  | jq -r '.items[0].id' \
339
- | xargs -I{} curviate recruiter applicant {} --account acc_1
341
+ | xargs -I{} curviate recruiter applicant "$PROJECT_ID" {} --account acc_1
340
342
  ```
341
343
 
342
- ### 4. Download an applicant's resume
344
+ ### 5. Download an applicant's resume
343
345
 
344
346
  ```bash
345
- curviate recruiter applicant resume APPLICANT_ID --account acc_1 -o resume.pdf
347
+ curviate recruiter applicant resume "$PROJECT_ID" APPLICANT_ID --account acc_1 -o resume.pdf
346
348
  ```
347
349
 
348
- ### 5. Reject an applicant, optionally notifying them
349
-
350
- The applicant is only notified when `--message` is given; omit it to reject silently.
351
- `--notify-at` (a UNIX-ms timestamp to schedule the notification) requires `--message`.
350
+ ### 6. Save a candidate to a project pipeline stage
352
351
 
353
352
  ```bash
354
- # Silent rejection no notification sent
355
- curviate recruiter reject-applicant AEM789 \
356
- --account acc_1 \
357
- --hiring-project-id proj_abc \
358
- --reason NOT_MEET_BASIC_QUALIFICATIONS
359
-
360
- # Rejection with a notification to the applicant
361
- curviate recruiter reject-applicant AEM789 \
353
+ curviate recruiter save-candidate "$PROJECT_ID" \
362
354
  --account acc_1 \
363
- --hiring-project-id proj_abc \
364
- --reason NOT_MEET_BASIC_QUALIFICATIONS \
365
- --message "Thanks for applying — we've decided to move forward with other candidates." \
366
- --preview
355
+ --stage-id "$STAGE_ID" \
356
+ --candidate-id AEM789
367
357
  ```
368
358
 
369
- ### 6. Search Recruiter people
359
+ ### 7. Search Recruiter people
370
360
 
371
361
  ```bash
372
362
  curviate recruiter search people \
@@ -374,33 +364,14 @@ curviate recruiter search people \
374
364
  --account acc_1 \
375
365
  --limit 5 --json \
376
366
  | jq -r '.items[] | "\(.id)\t\(.full_name // .headline)"'
377
- ```
378
367
 
379
- ### 7. Add a candidate to a hiring project, then promote them to applicant
380
-
381
- ```bash
382
- curviate recruiter add-candidate AEM789 \
383
- --account acc_1 \
384
- --hiring-project-id proj_abc \
385
- --stage UNCONTACTED
386
-
387
- # Once they've applied, move them to the applicant pool:
388
- curviate recruiter add-applicant AEM789 \
389
- --account acc_1 \
390
- --hiring-project-id proj_abc \
391
- --stage CONTACTED
368
+ # A pasted Recruiter search / talent-pool URL runs directly:
369
+ curviate recruiter search "https://www.linkedin.com/talent/search?..." --account acc_1 --json
392
370
  ```
393
371
 
394
- ### 8. Inspect a single hiring project, then list its job postings
395
-
396
- ```bash
397
- curviate recruiter project proj_abc --account acc_1 --json
398
-
399
- curviate recruiter jobs --account acc_1 --limit 10 --json \
400
- | jq -r '.items[] | "\(.id)\t\(.title)\t\(.state)"'
401
- ```
372
+ ### 8. Get a Recruiter-enriched profile, then start a chat with them
402
373
 
403
- ### 9. Get a Recruiter-enriched profile, then start a chat with them
374
+ `recruiter message new` is JSON-only and requires `--subject` and `--signature`.
404
375
 
405
376
  ```bash
406
377
  curviate recruiter profile "https://www.linkedin.com/in/example" --account acc_1 --json
@@ -408,20 +379,22 @@ curviate recruiter profile "https://www.linkedin.com/in/example" --account acc_1
408
379
  curviate recruiter message new \
409
380
  --to AEM789 \
410
381
  --account acc_1 \
382
+ --subject "A role you'd be a great fit for" \
383
+ --signature "— Alex, Talent Team" \
411
384
  "Hi — I came across your profile and think you'd be a great fit for a role we're hiring for."
412
385
  ```
413
386
 
414
- ### 10. Get any public job posting through the Recruiter lens
387
+ ### 9. List your postings, and get any public job posting through the Recruiter lens
415
388
 
416
389
  Unlike `recruiter jobs` (which lists postings you manage), `recruiter job get` retrieves the full
417
390
  detail of *any* public LinkedIn job posting — the Recruiter-seated counterpart to the top-level
418
391
  `job get` command:
419
392
 
420
393
  ```bash
421
- curviate recruiter job get "https://www.linkedin.com/jobs/view/4428113858" --account acc_1 --json
394
+ curviate recruiter jobs --account acc_1 --limit 10 --json \
395
+ | jq -r '.items[] | "\(.id)\t\(.title)\t\(.state)"'
422
396
 
423
- # Bare numeric id works identically:
424
- curviate recruiter job get 4428113858 --account acc_1 --verbose
397
+ curviate recruiter job get "https://www.linkedin.com/jobs/view/4428113858" --account acc_1 --json
425
398
  ```
426
399
 
427
400
  ## Exit codes