Skip to content

Commit 3c81bda

Browse files
gnapseclaude
andauthored
fix(person): Support name lookup and id: prefix syntax (#13)
## Summary - Fix missing Content-Type header for POST requests without body - Add name-based person lookup via search API (exact then partial matching) - Require explicit `id:xxx` prefix for ID lookups to eliminate ambiguity - Support email addresses for direct lookup - Comprehensive help text and error hints for AI agent usability ## Test plan - `bob person Ernesto` → name search - `bob person id:12345` → direct ID lookup - `bob person user@co.com` → direct email lookup - All 27 existing tests pass 🤖 Generated with Claude Code --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 2bacc78 commit 3c81bda

File tree

2 files changed

+82
-8
lines changed

2 files changed

+82
-8
lines changed

src/commands/people.ts

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,75 @@ async function listPeople(query: string | undefined, options: PeopleOptions): Pr
183183
outputList(people, options, 'person', renderPersonRow)
184184
}
185185

186-
async function viewPerson(id: string, options: OutputOptions): Promise<void> {
187-
if (!id) {
188-
throw new Error(formatError('MISSING_ID', 'Person id is required.'))
186+
function isIdRef(ref: string): boolean {
187+
return ref.startsWith('id:')
188+
}
189+
190+
function removeIdPrefix(ref: string): string {
191+
return ref.slice(3)
192+
}
193+
194+
function looksLikeRawId(ref: string): boolean {
195+
if (ref.includes(' ')) return false
196+
return /^\d+$/.test(ref)
197+
}
198+
199+
async function fetchPersonByIdentifier(id: string): Promise<Record<string, unknown>> {
200+
const response = await apiPost(`/people/${id}`, {})
201+
return extractPerson(response)
202+
}
203+
204+
async function resolvePersonRef(ref: string): Promise<Record<string, unknown>> {
205+
if (isIdRef(ref)) {
206+
return fetchPersonByIdentifier(removeIdPrefix(ref))
207+
}
208+
209+
// HiBob API accepts email as an identifier in /people/{identifier}
210+
if (ref.includes('@')) {
211+
return fetchPersonByIdentifier(ref)
212+
}
213+
214+
if (looksLikeRawId(ref)) {
215+
return fetchPersonByIdentifier(ref)
189216
}
190-
const response = await apiPost(`/people/${id}`)
191-
const person = extractPerson(response)
217+
218+
const response = await apiPost('/people/search', {})
219+
const people = extractPeopleList(response).filter((p) => isActive(p) !== false)
220+
const lower = ref.toLowerCase()
221+
222+
const exact = people.find((p) => getDisplayName(p).toLowerCase() === lower)
223+
if (exact) {
224+
const id = exact.id
225+
if (typeof id === 'string' && id) return fetchPersonByIdentifier(id)
226+
return exact
227+
}
228+
229+
const partial = people.filter((p) => getDisplayName(p).toLowerCase().includes(lower))
230+
if (partial.length === 1) {
231+
const id = partial[0].id
232+
if (typeof id === 'string' && id) return fetchPersonByIdentifier(id)
233+
return partial[0]
234+
}
235+
if (partial.length > 1) {
236+
throw new Error(
237+
formatError(
238+
'AMBIGUOUS_PERSON',
239+
`Multiple people match "${ref}":`,
240+
partial.slice(0, 5).map((p) => `"${getDisplayName(p)}" (id:${p.id})`),
241+
),
242+
)
243+
}
244+
245+
throw new Error(formatError('PERSON_NOT_FOUND', `Person "${ref}" not found.`))
246+
}
247+
248+
async function viewPerson(ref: string, options: OutputOptions): Promise<void> {
249+
if (!ref) {
250+
throw new Error(
251+
formatError('MISSING_REF', 'Person ref is required (name, email, or id:xxx).'),
252+
)
253+
}
254+
const person = await resolvePersonRef(ref)
192255
outputItem(person, options, 'person', renderPersonView)
193256
}
194257

@@ -207,10 +270,20 @@ export function registerPeopleCommand(program: Command): void {
207270
program
208271
.command('person')
209272
.description('View a single employee')
210-
.argument('<id>', 'Employee id')
273+
.argument('<ref>', 'Name, email, or id:xxx')
211274
.option('--json', 'JSON output (essential fields)')
212275
.option('--ndjson', 'NDJSON output (essential fields)')
213276
.option('--full', 'Include all fields in JSON output')
277+
.addHelpText(
278+
'after',
279+
`
280+
Ref formats:
281+
id:xxx Direct lookup by HiBob employee ID (exact, fastest)
282+
user@co.com Direct lookup by email address
283+
"Jane Doe" Name search (exact match preferred, partial if unambiguous)
284+
285+
Use "bob people" to list employees and find id:xxx values.`,
286+
)
214287
.action((id: string, options: OutputOptions) => viewPerson(id, options))
215288

216289
people.addHelpText(

src/lib/skills/content.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Use this skill when the user wants to query HiBob HR data.
1010
1111
- \`bob people\` - List employees
1212
- \`bob people "john"\` - Search employees by name (local filter)
13-
- \`bob person <id>\` - View a single employee
13+
- \`bob person <ref>\` - View a single employee (name, email, or id:xxx)
1414
- \`bob whosout\` - Who is out of office
1515
- \`bob outtoday\` - Who is out today
1616
- \`bob skill list\` - List supported agents
@@ -27,7 +27,8 @@ All list commands support:
2727
\`\`\`bash
2828
bob people --json
2929
bob people "Ava" --department "Engineering"
30-
bob person 12345
30+
bob person "Jane Doe"
31+
bob person id:12345
3132
bob whosout --from 2024-01-15 --to 2024-01-20
3233
bob outtoday --date 2024-01-15
3334
\`\`\`

0 commit comments

Comments
 (0)