Skip to content

Commit 41da13c

Browse files
authored
feat: add search command MVP (#26)
* feat: add search command MVP * test: add search to smoke script * feat: include location in normalized connection outputs * feat: add mutualConnections metadata to search results * docs: record search parser mutual/location learnings
1 parent 5bb0e32 commit 41da13c

File tree

15 files changed

+1074
-9
lines changed

15 files changed

+1074
-9
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,31 @@ li connections --of peggyrayzis # View someone else's connections
7474

7575
Note: `li connections --of ...` depends on LinkedIn search pagination behavior. LinkedIn may cap results (often around ~20) even when more pages are requested.
7676

77+
### Search
78+
79+
```bash
80+
li search --query "founder" # Search profiles
81+
li search --query "founder" -n 10 # Return up to 10
82+
li search --query "founder" --all # Request all pages (still capped at 50)
83+
li search --query "founder" --fast # Faster fetch mode
84+
li search --query "founder" --json # Machine-readable output
85+
```
86+
87+
Supported flags: `--json`, `--fast`, `-n/--count`, `--all`.
88+
89+
Temporary MVP limit: search results are hard-capped at 50 profiles. `--all` still stops at 50.
90+
91+
MVP JSON output for `li search --query ... --json` uses these top-level keys:
92+
93+
```json
94+
{
95+
"query": "founder",
96+
"limitApplied": 50,
97+
"connections": [],
98+
"paging": {}
99+
}
100+
```
101+
77102
### Invitations
78103

79104
```bash

docs/MEMORY.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ Append-only log. Add entries; do not rewrite historical entries except typo fixe
88
- Fix: updated URL parsing to normalize no-scheme LinkedIn URLs before route parsing, and added tests for no-scheme supported/unsupported paths.
99
- Guardrail: recipient cache warnings now ignore synthetic/non-canonical URNs (for example fixture-style placeholders) and tests use isolated cache paths to prevent cross-run cache contamination.
1010

11+
## 2026-02-27
12+
- Correction: people-search parsing can misclassify text when social-proof metadata is split across larger result blocks (for example truncated headline fragments or missing mutual-connection signals in short chunks).
13+
- Fix: increased bounded result-chunk scan length for search stream parsing, tightened candidate filtering for headline/location extraction, and derived `mutualConnections` from social-proof windows nearest mutual text.
14+
- Guardrail: parser + command tests now pin mutual-connection extraction behavior, and smoke runs verify live search JSON shape remains stable under real payload variants.
15+
1116
## 2026-02-25
1217
- Correction: process guidance was fragmented across root markdown files and drifted over time.
1318
- Fix: consolidated process rules into `AGENTS.md`, moved specification to `docs/SPEC.md`, and started managed freshness checks.

docs/SPEC.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
# li Specification
2-
Last Updated: 2026-02-25
2+
Last Updated: 2026-02-27
33

44
## Product Goal
55
`li` is a Node.js TypeScript CLI for LinkedIn workflows with human-readable output and machine-readable `--json` output.
66

77
## Scope (Current)
8-
- Read-focused account and network workflows (`whoami`, `check`, `profile`, `connections`, `invites`, `messages`).
8+
- Read-focused account and network workflows (`whoami`, `check`, `profile`, `connections`, `search`, `invites`, `messages`).
99
- Cookie-based authentication via browser/session credentials.
1010
- Voyager endpoint support and normalization utilities.
1111

12+
## Search Command (MVP)
13+
- Command: `li search --query <text>`
14+
- Supported flags: `--json`, `--fast`, `-n/--count`, `--all`
15+
- Temporary hard cap: maximum 50 profiles per search response.
16+
- `--all` requests pagination but is still capped at 50 profiles in MVP.
17+
- JSON output is intentionally minimal in MVP and currently uses top-level keys:
18+
`{ query, limitApplied, connections, paging }`.
19+
1220
## Out Of Scope (Current)
1321
- Unreviewed write actions that may mutate LinkedIn state by default.
1422
- Long-lived internal process docs outside `AGENTS.md` + `docs/`.
@@ -28,6 +36,7 @@ Last Updated: 2026-02-25
2836
## Known Limitations
2937
- `connections --of` relies on upstream LinkedIn search pagination behavior that can be account/session capped (often around two pages, ~20 results) even when additional page requests return HTTP 200.
3038
- `--all` for `connections --of` should be treated as potentially partial under active upstream clamp conditions.
39+
- `search` is currently limited to a maximum of 50 profiles regardless of requested count or `--all`.
3140

3241
## Related References
3342
- Process rules: `AGENTS.md`

scripts/smoke.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ cd "$ROOT_DIR"
66

77
OTHER_PROFILE_URL="${LI_SMOKE_OTHER_PROFILE_URL:-https://www.linkedin.com/in/mnmagan}"
88
COUNT="${LI_SMOKE_COUNT:-3}"
9+
SEARCH_QUERY="${LI_SMOKE_SEARCH_QUERY:-react developers in san francisco at AI companies}"
910

1011
run() {
1112
echo "==> $*"
@@ -44,6 +45,18 @@ run "${CLI[@]}" profile "https://www.linkedin.com/in/$OWN_USERNAME" --json
4445
run "${CLI[@]}" connections -n "$COUNT" --json
4546
run "${CLI[@]}" connections -n "$COUNT" --json --of "$OTHER_PROFILE_URL"
4647

48+
echo "==> ${CLI[*]} search --query \"$SEARCH_QUERY\" -n $COUNT --json"
49+
SEARCH_JSON="$("${CLI[@]}" search --query "$SEARCH_QUERY" -n "$COUNT" --json)"
50+
printf "%s" "$SEARCH_JSON" | node -e '
51+
const fs = require("node:fs");
52+
const data = JSON.parse(fs.readFileSync(0, "utf8"));
53+
if (!Object.prototype.hasOwnProperty.call(data, "query")) process.exit(1);
54+
if (!Object.prototype.hasOwnProperty.call(data, "limitApplied")) process.exit(1);
55+
if (!Array.isArray(data.connections)) process.exit(1);
56+
if (!data.paging || typeof data.paging !== "object") process.exit(1);
57+
console.log(` query="${data.query}" results=${data.connections.length}`);
58+
'
59+
4760
echo "==> ${CLI[*]} messages -n $COUNT --json"
4861
MESSAGES_JSON="$("${CLI[@]}" messages -n "$COUNT" --json)"
4962
CONVO_ID="$(

src/cli.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { listInvites } from "./commands/invites.js";
1616
import { listConversations, readConversation } from "./commands/messages.js";
1717
import { profile } from "./commands/profile.js";
1818
import { queryIds } from "./commands/query-ids.js";
19+
import { search } from "./commands/search.js";
1920
import { whoami } from "./commands/whoami.js";
2021
import { type BrowserSource, resolveCredentials } from "./lib/auth.js";
2122

@@ -37,6 +38,7 @@ const COMMAND_NAMES = new Set([
3738
"whoami",
3839
"check",
3940
"profile",
41+
"search",
4042
"connections",
4143
"invites",
4244
"messages",
@@ -332,6 +334,38 @@ program
332334
}
333335
});
334336

337+
// ============================================================================
338+
// search - Search profiles
339+
// ============================================================================
340+
program
341+
.command("search")
342+
.description("Search LinkedIn profiles")
343+
.option("--query <text>", "Search query text")
344+
.option("--json", "Output as JSON")
345+
.option("-n, --count <number>", "Number of profiles to show", "20")
346+
.option("--all", "Fetch all available profiles (capped at 50)")
347+
.option("--fast", "Faster pacing with adaptive slowdown on rate limits")
348+
.action(async (options) => {
349+
try {
350+
if (!options.query || options.query.trim() === "") {
351+
throw new Error("Missing required option: --query <text>");
352+
}
353+
const globalOpts = program.opts();
354+
const credentials = await getCredentials(globalOpts);
355+
const output = await search(credentials, {
356+
query: options.query,
357+
json: options.json,
358+
all: options.all,
359+
count: Number.parseInt(options.count, 10),
360+
fast: options.fast,
361+
noProgress: globalOpts.progress === false,
362+
});
363+
console.log(output);
364+
} catch (error) {
365+
handleError(error);
366+
}
367+
});
368+
335369
// ============================================================================
336370
// connections - List connections
337371
// ============================================================================

0 commit comments

Comments
 (0)