@forwardimpact/outpost 3.12.0 → 3.13.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 +2 -2
- package/bin/fit-outpost.js +3 -2
- package/package.json +6 -5
- package/src/agent-path.js +10 -6
- package/src/agent-runner.js +28 -24
- package/src/index.js +2 -3
- package/src/kb-manager.js +75 -20
- package/src/kb-validator.js +762 -0
- package/src/outpost.js +108 -39
- package/src/posture.js +3 -3
- package/src/privilege.js +11 -11
- package/src/scheduler.js +14 -12
- package/src/socket-server.js +19 -18
- package/src/spawn-env.js +7 -6
- package/src/state-manager.js +2 -2
- package/templates/.claude/agents/chief-of-staff.md +32 -24
- package/templates/.claude/agents/concierge.md +26 -17
- package/templates/.claude/agents/head-hunter.md +34 -26
- package/templates/.claude/agents/librarian.md +26 -18
- package/templates/.claude/agents/postman.md +26 -18
- package/templates/.claude/agents/recruiter.md +38 -27
- package/templates/.claude/skills/anarlog-follow/SKILL.md +91 -66
- package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
- package/templates/.claude/skills/anarlog-follow/references/sessions.md +52 -0
- package/templates/.claude/skills/anarlog-process/SKILL.md +108 -62
- package/templates/.claude/skills/anarlog-process/references/extraction.md +41 -29
- package/templates/.claude/skills/anarlog-process/references/sessions.md +92 -58
- package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +402 -160
- package/templates/.claude/skills/candidate-report/SKILL.md +41 -36
- package/templates/.claude/skills/candidate-report/references/report-template.html +10 -10
- package/templates/.claude/skills/candidate-report/references/report.css +4 -4
- package/templates/.claude/skills/candidate-report/references/rubric.md +15 -14
- package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +2 -2
- package/templates/.claude/skills/changelog/SKILL.md +122 -67
- package/templates/.claude/skills/deck-create/SKILL.md +90 -32
- package/templates/.claude/skills/deck-create/references/slide.css +4 -4
- package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/deck-review/SKILL.md +63 -58
- package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
- package/templates/.claude/skills/deck-summarize/SKILL.md +55 -45
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +5 -4
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
- package/templates/.claude/skills/doc-collab/SKILL.md +39 -34
- package/templates/.claude/skills/doc-create/SKILL.md +27 -23
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/draft-emails/SKILL.md +63 -53
- package/templates/.claude/skills/draft-emails/references/template.md +2 -2
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +15 -8
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +19 -11
- package/templates/.claude/skills/extract-entities/SKILL.md +76 -75
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +6 -5
- package/templates/.claude/skills/extract-entities/references/conditions.md +20 -18
- package/templates/.claude/skills/extract-entities/references/content.md +10 -9
- package/templates/.claude/skills/extract-entities/references/links.md +37 -13
- package/templates/.claude/skills/extract-entities/references/recruitment.md +30 -24
- package/templates/.claude/skills/extract-entities/references/resolution.md +14 -14
- package/templates/.claude/skills/extract-entities/references/sources.md +9 -9
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +12 -6
- package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +29 -10
- package/templates/.claude/skills/extract-entities/references/templates-priorities.md +10 -4
- package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +19 -7
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
- package/templates/.claude/skills/meeting-prep/SKILL.md +42 -36
- package/templates/.claude/skills/organize-files/SKILL.md +27 -22
- package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
- package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
- package/templates/.claude/skills/person-identify/SKILL.md +77 -27
- package/templates/.claude/skills/person-identify/scripts/identify.sh +125 -26
- package/templates/.claude/skills/person-lookup/SKILL.md +34 -30
- package/templates/.claude/skills/person-lookup/scripts/lookup.sh +46 -17
- package/templates/.claude/skills/req-assess/SKILL.md +44 -36
- package/templates/.claude/skills/req-assess/references/interview-template.md +7 -2
- package/templates/.claude/skills/req-assess/references/panel-template.md +7 -2
- package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
- package/templates/.claude/skills/req-bundle/SKILL.md +164 -0
- package/templates/.claude/skills/req-bundle/references/matching.md +70 -0
- package/templates/.claude/skills/req-bundle/references/pdf-structure.md +74 -0
- package/templates/.claude/skills/req-bundle/scripts/split-bundle.mjs +377 -0
- package/templates/.claude/skills/req-decide/SKILL.md +52 -41
- package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
- package/templates/.claude/skills/req-decide/references/template.md +2 -2
- package/templates/.claude/skills/req-forget/SKILL.md +58 -39
- package/templates/.claude/skills/req-forget/references/classify.md +15 -13
- package/templates/.claude/skills/req-forget/references/locations.md +27 -22
- package/templates/.claude/skills/req-forget/references/report-template.md +24 -16
- package/templates/.claude/skills/req-scan/SKILL.md +27 -25
- package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
- package/templates/.claude/skills/req-scan/references/filters.md +13 -13
- package/templates/.claude/skills/req-scan/references/sources.md +7 -7
- package/templates/.claude/skills/req-scan/references/state.md +1 -1
- package/templates/.claude/skills/req-scan/references/template.md +11 -1
- package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
- package/templates/.claude/skills/req-screen/SKILL.md +96 -50
- package/templates/.claude/skills/req-screen/references/rubric.md +46 -50
- package/templates/.claude/skills/req-screen/references/scoring.md +79 -0
- package/templates/.claude/skills/req-screen/references/template.md +29 -17
- package/templates/.claude/skills/req-track/SKILL.md +76 -76
- package/templates/.claude/skills/req-track/references/fields.md +31 -27
- package/templates/.claude/skills/req-track/references/overlays.md +42 -0
- package/templates/.claude/skills/req-track/references/signals.md +12 -12
- package/templates/.claude/skills/req-track/references/statuses.md +3 -2
- package/templates/.claude/skills/req-track/references/templates.md +28 -27
- package/templates/.claude/skills/req-workday/SKILL.md +69 -40
- package/templates/.claude/skills/req-workday/references/brief.md +81 -0
- package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
- package/templates/.claude/skills/req-workday/references/templates.md +11 -78
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +26 -7
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +113 -35
- package/templates/.claude/skills/send-chat/SKILL.md +58 -54
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +23 -19
- package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
- package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
- package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +20 -14
- package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
- package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
- package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
- package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
- package/templates/.claude/skills/sync-teams/SKILL.md +46 -43
- package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
- package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
- package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
- package/templates/.claude/skills/upstream-instructions/SKILL.md +43 -41
- package/templates/.claude/skills/upstream-instructions/references/examples.md +5 -5
- package/templates/CLAUDE.md +97 -85
- package/templates/MIGRATION.md +359 -0
- package/templates/registry.yaml +25 -0
- package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +0 -243
- package/templates/.claude/skills/anarlog-trim/SKILL.md +0 -183
|
@@ -1,31 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: person-identify
|
|
3
|
-
description: Look up the current user's identity (real name, company, job title, department, email, employee ID, and
|
|
3
|
+
description: Look up the current user's identity (real name, company, job title, department, email, employee ID, manager, and direct reports) from the corporate directory through LDAP. The bind uses the existing Kerberos ticket. Captures the user's org edges — manager and direct reports — so the KB can resolve who "our team" is. Use to establish who the knowledge base belongs to, when CLAUDE.md needs the user's identity, or when the user asks "who am I" / for their own directory record. To look up someone *other* than the current user, use the sibling `person-lookup` skill instead.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Person Identify
|
|
7
7
|
|
|
8
|
+
Write tier: none (cache only)
|
|
9
|
+
Frontmatter: none
|
|
10
|
+
|
|
8
11
|
Resolve the current user's identity from the corporate Active Directory over
|
|
9
12
|
LDAP. This is the canonical way to establish **who the knowledge base belongs
|
|
10
|
-
to
|
|
11
|
-
so a job change or reorg
|
|
13
|
+
to**. It replaces any static identity file. Results reflect the live
|
|
14
|
+
directory, so the skill picks up a job change or a reorg automatically.
|
|
12
15
|
|
|
13
16
|
## Trigger
|
|
14
17
|
|
|
15
|
-
- CLAUDE.md (or another skill) needs the current user's identity
|
|
18
|
+
- CLAUDE.md (or another skill) needs the current user's identity, or needs to
|
|
19
|
+
resolve who "our team" is.
|
|
16
20
|
- The user asks "who am I", for their own directory record, or for their
|
|
17
|
-
employee ID, title, department, or
|
|
21
|
+
employee ID, title, department, manager, or direct reports.
|
|
18
22
|
|
|
19
23
|
## Prerequisites
|
|
20
24
|
|
|
21
25
|
- A valid **Kerberos ticket** for the user (`klist` shows a principal).
|
|
22
26
|
If absent, get one with `kinit <user>@<REALM>`.
|
|
23
|
-
- Network
|
|
24
|
-
- `ldapsearch` and `dig` — both ship with macOS
|
|
27
|
+
- Network access to a domain controller (on-site or through VPN).
|
|
28
|
+
- `ldapsearch` and `dig` — both ship with macOS. You install nothing.
|
|
25
29
|
|
|
26
|
-
Nothing is hardcoded
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
Nothing is hardcoded. The script derives the username, realm, base DN, and
|
|
31
|
+
domain controller at runtime from the ticket and DNS. You never enter a
|
|
32
|
+
password. The bind uses SASL/GSSAPI against the existing ticket.
|
|
29
33
|
|
|
30
34
|
## Usage
|
|
31
35
|
|
|
@@ -33,27 +37,39 @@ the bind uses SASL/GSSAPI against the existing ticket.
|
|
|
33
37
|
bash .claude/skills/person-identify/scripts/identify.sh
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
This prints the user's directory record, resolves the manager
|
|
37
|
-
writes the result to the identity cache (below).
|
|
40
|
+
This prints the user's directory record, resolves the manager and each direct
|
|
41
|
+
report to a name, and writes the result to the identity cache (below).
|
|
38
42
|
|
|
39
43
|
## Identity cache
|
|
40
44
|
|
|
41
|
-
The script writes `~/.cache/fit/outpost/state/identity.md
|
|
42
|
-
identity source** for the rest of the knowledge base
|
|
43
|
-
`USER.md`. It is auto-generated markdown with `Name`, `Email`,
|
|
44
|
-
fields (plus title, department, company, employee ID, office, and
|
|
45
|
+
The script writes `~/.cache/fit/outpost/state/identity.md`. That file is the
|
|
46
|
+
**canonical identity source** for the rest of the knowledge base. It replaces
|
|
47
|
+
the old static `USER.md`. It is auto-generated markdown with `Name`, `Email`,
|
|
48
|
+
and `Domain` fields (plus title, department, company, employee ID, office, and
|
|
49
|
+
the user's two org edges — `Manager` and `Direct reports`):
|
|
45
50
|
|
|
46
51
|
```markdown
|
|
47
52
|
- **Name:** Jane Doe
|
|
48
53
|
- **Email:** jane.doe@example.com
|
|
49
54
|
- **Domain:** example.com
|
|
55
|
+
- **Manager:** Roe, Richard
|
|
56
|
+
- **Direct reports:**
|
|
57
|
+
- Chen, Sarah
|
|
58
|
+
- Okafor, Ada
|
|
50
59
|
```
|
|
51
60
|
|
|
61
|
+
The **Manager** and **Direct reports** edges let the KB resolve who "our team"
|
|
62
|
+
is — see the `## User Identity & Team` section of `CLAUDE.md` for the rule.
|
|
63
|
+
`Direct reports` is always written: a list when the user manages people, or
|
|
64
|
+
the explicit sentinel `- **Direct reports:** none` for an individual
|
|
65
|
+
contributor. Reports are name-sorted so re-runs of the synced cache diff
|
|
66
|
+
cleanly.
|
|
67
|
+
|
|
52
68
|
Other skills (e.g. `extract-entities`, `anarlog-process`, `req-track`,
|
|
53
69
|
`req-workday`, `candidate-report`, `sync-teams`) read this file for the user's
|
|
54
|
-
name/email/domain
|
|
55
|
-
skill first if the cache is missing or stale. Never hand-edit the
|
|
56
|
-
the skill to refresh it.
|
|
70
|
+
name/email/domain. They use it for self-exclusion and author attribution. They
|
|
71
|
+
run this skill first if the cache is missing or stale. Never hand-edit the
|
|
72
|
+
cache. Run the skill again to refresh it.
|
|
57
73
|
|
|
58
74
|
## How it works
|
|
59
75
|
|
|
@@ -73,11 +89,29 @@ dc=$(dig +short SRV "_ldap._tcp.dc._msdcs.$dom" | awk 'NR==1{print $4}' | sed 's
|
|
|
73
89
|
|
|
74
90
|
# 4. Look up the current user (GSSAPI = existing ticket, no password)
|
|
75
91
|
ldapsearch -Y GSSAPI -LLL -o ldif-wrap=no -H "ldap://$dc" -b "$base" \
|
|
76
|
-
"(sAMAccountName=$user)" displayName company title department employeeID mail
|
|
92
|
+
"(sAMAccountName=$user)" displayName company title department employeeID mail \
|
|
93
|
+
manager directReports
|
|
77
94
|
```
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
resolves
|
|
96
|
+
Both org edges come back as DNs that may live in another domain. So the script
|
|
97
|
+
resolves them against the **Global Catalog** (port 3268, forest-wide) through
|
|
98
|
+
one shared `dn_name` helper. `manager` is single-valued. `directReports` is
|
|
99
|
+
the multi-valued back-link of the same edge, so the script pulls every value
|
|
100
|
+
(not just the first), resolves each to a name, and sorts the list. The
|
|
101
|
+
resolver decodes the base64 `displayName:: …` form (used for accented names)
|
|
102
|
+
and retries past the directory's occasional attribute-less response under
|
|
103
|
+
load.
|
|
104
|
+
|
|
105
|
+
**Inactive reports are filtered from the roster.** AD does not clear the
|
|
106
|
+
`manager` edge when an account is offboarded — it just moves the account to a
|
|
107
|
+
deprovisioned OU — so the user's `directReports` back-link keeps counting
|
|
108
|
+
ex-reports as phantom team members. Reports resolve through `dn_report`, which
|
|
109
|
+
drops a DN when either (1) it sits under the obsolete OU
|
|
110
|
+
(`INACTIVE_OU_PATTERN`, a config knob at the top of the script; `OU=Obsolete`
|
|
111
|
+
by default, empty to disable), or (2) the account is disabled in AD —
|
|
112
|
+
`userAccountControl` bit `0x2` (`ACCOUNTDISABLE`), fetched in the same GC
|
|
113
|
+
query as the name. The manager edge stays unfiltered: a disabled manager is a
|
|
114
|
+
signal worth surfacing, not hiding.
|
|
81
115
|
|
|
82
116
|
## Output
|
|
83
117
|
|
|
@@ -93,12 +127,28 @@ Key attributes returned (names per Active Directory schema):
|
|
|
93
127
|
| `mail` | Email address |
|
|
94
128
|
| `physicalDeliveryOfficeName`| Office / location |
|
|
95
129
|
| `manager` | Manager (DN → resolved to name) |
|
|
130
|
+
| `directReports` | Direct reports (multi-valued DN back-link → each resolved to a name; offboarded/disabled accounts filtered out) |
|
|
131
|
+
| `userAccountControl` | Fetched per report to drop disabled accounts (bit `0x2`) |
|
|
96
132
|
|
|
97
133
|
## Notes
|
|
98
134
|
|
|
99
|
-
- To look up **someone else**, use the sibling `person-lookup` skill
|
|
100
|
-
free-text input (email or name)
|
|
101
|
-
(`ldap://$dc:3268 -b ''`)
|
|
135
|
+
- To look up **someone else**, use the sibling `person-lookup` skill. It takes
|
|
136
|
+
free-text input (email or name). It searches the Global Catalog forest-wide
|
|
137
|
+
(`ldap://$dc:3268 -b ''`). It handles multiple matches. It does **not** touch
|
|
102
138
|
the identity cache.
|
|
103
|
-
-
|
|
104
|
-
|
|
139
|
+
- `directReports` is the back-link stored on the user's own home-domain
|
|
140
|
+
record, so a report whose account lives in *another* forest domain may be
|
|
141
|
+
omitted. For a single-domain manager (the common case) it is complete. If
|
|
142
|
+
cross-domain reports matter, swap it for a forest-wide reverse search over
|
|
143
|
+
the GC — `(manager=<userDN>)`, where the user's DN is the `dn:` line of the
|
|
144
|
+
fetched record — feeding the same `dn_report` resolver.
|
|
145
|
+
- **Offboarded reports linger in AD.** The `directReports` back-link keeps
|
|
146
|
+
counting a report after they leave, because offboarding moves the account to
|
|
147
|
+
a deprovisioned OU without clearing its `manager` edge. `dn_report` filters
|
|
148
|
+
these out (obsolete-OU DN, or a disabled `userAccountControl`). Tune
|
|
149
|
+
`INACTIVE_OU_PATTERN` at the top of `scripts/identify.sh` to your
|
|
150
|
+
directory's convention — it mirrors `person-lookup`'s `VENDOR_OU_PATTERN`
|
|
151
|
+
knob. The real fix is upstream (clear the manager edge at offboarding); the
|
|
152
|
+
filter shields the KB until then.
|
|
153
|
+
- Active Directory is not required. The same `ldapsearch -Y GSSAPI` shape works
|
|
154
|
+
against any Kerberos-backed LDAP directory. Only the attribute names differ.
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Identify the current user from the corporate directory (Active Directory over LDAP)
|
|
3
|
-
#
|
|
2
|
+
# Identify the current user from the corporate directory (Active Directory over LDAP).
|
|
3
|
+
# Cache the result at ~/.cache/fit/outpost/state/identity.md for other skills.
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# Kerberos ticket
|
|
5
|
+
# The script is generic. Nothing is hardcoded. It derives the user, the realm,
|
|
6
|
+
# and the base DN from the existing Kerberos ticket. It finds the domain
|
|
7
|
+
# controller with DNS SRV records. It works for any AD domain, on or off VPN.
|
|
8
|
+
# It needs a Kerberos ticket and a reachable DC.
|
|
9
9
|
#
|
|
10
|
-
# Auth uses SASL/GSSAPI against the existing ticket
|
|
10
|
+
# Auth uses SASL/GSSAPI against the existing ticket. You never enter a password.
|
|
11
11
|
set -u
|
|
12
12
|
|
|
13
13
|
CACHE="$HOME/.cache/fit/outpost/state/identity.md"
|
|
14
14
|
|
|
15
|
-
#
|
|
15
|
+
# Org-specific: substring of the DN's OU under which offboarded/deprovisioned
|
|
16
|
+
# accounts are parked. AD keeps the `manager` edge on such accounts when they
|
|
17
|
+
# are moved here at offboarding, so the user's `directReports` back-link still
|
|
18
|
+
# counts them — surfacing ex-reports as phantom team members. Reports whose DN
|
|
19
|
+
# matches this pattern are dropped. Set to your directory's convention (empty
|
|
20
|
+
# disables the OU filter; disabled-account filtering via userAccountControl
|
|
21
|
+
# still applies). `OU=Obsolete` is a common AD default.
|
|
22
|
+
INACTIVE_OU_PATTERN="OU=Obsolete"
|
|
23
|
+
|
|
24
|
+
# 1. Take the user principal from the Kerberos ticket: USER@REALM.EXAMPLE.COM
|
|
16
25
|
princ=$(klist 2>/dev/null | sed -n 's/.*[Pp]rincipal: *//p' | head -1)
|
|
17
26
|
if [ -z "$princ" ]; then
|
|
18
27
|
echo "No Kerberos ticket found. Get one first, e.g.: kinit <user>@<REALM>" >&2
|
|
@@ -25,52 +34,132 @@ realm=${princ#*@}
|
|
|
25
34
|
base=$(printf '%s' "$realm" | awk -F. '{for(i=1;i<=NF;i++) printf "%sDC=%s",(i>1?",":""),$i}')
|
|
26
35
|
dom=$(printf '%s' "$realm" | tr '[:upper:]' '[:lower:]')
|
|
27
36
|
|
|
28
|
-
# 3. Find a reachable domain controller
|
|
37
|
+
# 3. Find a reachable domain controller with DNS SRV (msdcs first, then plain ldap)
|
|
29
38
|
dc=$(dig +short SRV "_ldap._tcp.dc._msdcs.$dom" | awk 'NR==1{print $4}' | sed 's/\.$//')
|
|
30
39
|
[ -z "$dc" ] && dc=$(dig +short SRV "_ldap._tcp.$dom" | awk 'NR==1{print $4}' | sed 's/\.$//')
|
|
31
40
|
if [ -z "$dc" ]; then
|
|
32
|
-
echo "Could not find a domain controller for $dom
|
|
41
|
+
echo "Could not find a domain controller for $dom with DNS SRV." >&2
|
|
33
42
|
exit 1
|
|
34
43
|
fi
|
|
35
44
|
|
|
36
45
|
# 4. Look up the current user's record (GSSAPI = use the existing ticket, no password)
|
|
37
|
-
attrs="displayName givenName sn company title department employeeID mail telephoneNumber physicalDeliveryOfficeName manager"
|
|
46
|
+
attrs="displayName givenName sn company title department employeeID mail telephoneNumber physicalDeliveryOfficeName manager directReports"
|
|
38
47
|
rec=$(ldapsearch -Y GSSAPI -LLL -o ldif-wrap=no -H "ldap://$dc" -b "$base" "(sAMAccountName=$user)" $attrs 2>/dev/null)
|
|
39
48
|
if [ -z "$rec" ]; then
|
|
40
49
|
echo "No directory record found for '$user' under $base on $dc." >&2
|
|
41
50
|
exit 1
|
|
42
51
|
fi
|
|
43
52
|
|
|
44
|
-
# Pull a single attribute value out of the LDIF record.
|
|
45
|
-
|
|
53
|
+
# Pull a single attribute value out of the LDIF record. Decode the base64
|
|
54
|
+
# `attr:: <b64>` form that LDIF uses for any non-ASCII value (accented names,
|
|
55
|
+
# and DNs whose CN is accented — e.g. the manager attribute).
|
|
56
|
+
field() { # $1=attr
|
|
57
|
+
local attr="$1" line
|
|
58
|
+
line=$(printf '%s\n' "$rec" | grep -m1 -E "^$attr:: ?|^$attr: ")
|
|
59
|
+
case "$line" in
|
|
60
|
+
"$attr:: "*) printf '%s' "${line#"$attr":: }" | base64 -D 2>/dev/null ;;
|
|
61
|
+
"$attr: "*) printf '%s' "${line#"$attr": }" ;;
|
|
62
|
+
esac
|
|
63
|
+
}
|
|
46
64
|
|
|
47
65
|
disp=$(field displayName); gn=$(field givenName); sn=$(field sn)
|
|
48
66
|
company=$(field company); title=$(field title); dept=$(field department)
|
|
49
67
|
empid=$(field employeeID); mail=$(field mail)
|
|
50
68
|
office=$(field physicalDeliveryOfficeName)
|
|
51
69
|
|
|
52
|
-
# Real name from given+surname
|
|
70
|
+
# Real name from given+surname. Fall back to displayName. Domain from the email.
|
|
53
71
|
name="$gn $sn"; name=$(printf '%s' "$name" | sed 's/^ *//;s/ *$//')
|
|
54
72
|
[ -z "$name" ] && name="$disp"
|
|
55
73
|
domain=$(printf '%s' "$mail" | sed 's/.*@//' | tr '[:upper:]' '[:lower:]')
|
|
56
74
|
|
|
57
|
-
# 5. Resolve the
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
# 5. Resolve the user's org edges — the manager (up) and the direct reports
|
|
76
|
+
# (down) — to names. Both are DNs that may live in another domain, so
|
|
77
|
+
# resolve against the Global Catalog (port 3268). It is forest-wide.
|
|
78
|
+
gc="ldap://$dc:3268"
|
|
79
|
+
|
|
80
|
+
# One shared DN -> displayName resolver. Decodes the base64 `displayName:: <b64>`
|
|
81
|
+
# form (accented names are encoded that way) and retries past the directory's
|
|
82
|
+
# occasional attribute-less response under load, mirroring person-lookup.
|
|
83
|
+
dn_name() { # $1=DN -> displayName ("" if unresolvable)
|
|
84
|
+
local dn="$1" line tries=0
|
|
85
|
+
[ -z "$dn" ] && return 0
|
|
86
|
+
while [ "$tries" -lt 4 ]; do
|
|
87
|
+
line=$(ldapsearch -Y GSSAPI -LLL -o ldif-wrap=no -H "$gc" -b "$dn" -s base \
|
|
88
|
+
displayName 2>/dev/null | grep -m1 -E '^displayName:: ?|^displayName: ')
|
|
89
|
+
case "$line" in
|
|
90
|
+
"displayName:: "*) printf '%s' "${line#displayName:: }" | base64 -D 2>/dev/null; return 0 ;;
|
|
91
|
+
"displayName: "*) printf '%s' "${line#displayName: }"; return 0 ;;
|
|
92
|
+
esac
|
|
93
|
+
tries=$((tries + 1)); sleep "$tries"
|
|
94
|
+
done
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Resolve a direct-report DN to a name, but ONLY if the account is still active.
|
|
98
|
+
# A report is inactive — and therefore dropped from the team roster — when either:
|
|
99
|
+
# 1. its DN sits under the obsolete/deprovisioned OU ($INACTIVE_OU_PATTERN), or
|
|
100
|
+
# 2. its account is disabled in AD (userAccountControl bit 0x2, ACCOUNTDISABLE).
|
|
101
|
+
# Offboarding leaves the `manager` edge dangling, so without this filter ex-reports
|
|
102
|
+
# linger in the roster forever. Fetches displayName + userAccountControl together,
|
|
103
|
+
# reusing dn_name's base64 decode and retry-under-load behaviour. Prints the name,
|
|
104
|
+
# or nothing to skip the report.
|
|
105
|
+
dn_report() { # $1=DN -> displayName if active, else ""
|
|
106
|
+
local dn="$1" out line uac tries=0
|
|
107
|
+
[ -z "$dn" ] && return 0
|
|
108
|
+
# (1) Obsolete-OU filter — case-insensitive substring match on the DN.
|
|
109
|
+
if [ -n "$INACTIVE_OU_PATTERN" ]; then
|
|
110
|
+
if printf '%s' "$dn" | grep -qiF "$INACTIVE_OU_PATTERN"; then return 0; fi
|
|
111
|
+
fi
|
|
112
|
+
while [ "$tries" -lt 4 ]; do
|
|
113
|
+
out=$(ldapsearch -Y GSSAPI -LLL -o ldif-wrap=no -H "$gc" -b "$dn" -s base \
|
|
114
|
+
displayName userAccountControl 2>/dev/null)
|
|
115
|
+
line=$(printf '%s\n' "$out" | grep -m1 -E '^displayName:: ?|^displayName: ')
|
|
116
|
+
if [ -n "$line" ]; then
|
|
117
|
+
# (2) Disabled-account filter. userAccountControl is a GC-replicated
|
|
118
|
+
# attribute; if absent (older DC / partial reply) we simply don't filter.
|
|
119
|
+
uac=$(printf '%s\n' "$out" | sed -n 's/^userAccountControl: *//p' | head -1)
|
|
120
|
+
case "$uac" in
|
|
121
|
+
''|*[!0-9]*) : ;; # missing/non-numeric -> can't judge
|
|
122
|
+
*) [ $((uac & 2)) -ne 0 ] && return 0 ;; # ACCOUNTDISABLE set -> skip
|
|
123
|
+
esac
|
|
124
|
+
case "$line" in
|
|
125
|
+
"displayName:: "*) printf '%s' "${line#displayName:: }" | base64 -D 2>/dev/null; return 0 ;;
|
|
126
|
+
"displayName: "*) printf '%s' "${line#displayName: }"; return 0 ;;
|
|
127
|
+
esac
|
|
128
|
+
fi
|
|
129
|
+
tries=$((tries + 1)); sleep "$tries"
|
|
130
|
+
done
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Up-edge: the manager (single-valued DN on the user's own record).
|
|
134
|
+
mgr_name=$(dn_name "$(field manager)")
|
|
135
|
+
|
|
136
|
+
# Down-edges: direct reports — a multi-valued back-link on the user's OWN record.
|
|
137
|
+
# field() keeps only the first value, so pull EVERY value: emit one report DN per
|
|
138
|
+
# line (decoding the base64 `directReports:: <b64>` form), resolve each to a name
|
|
139
|
+
# via dn_report (which drops offboarded/disabled accounts), and sort for stable,
|
|
140
|
+
# diff-friendly cache output. Kept in a function because a `case` inside `$(...)`
|
|
141
|
+
# trips the bash 3.2 parser that macOS ships.
|
|
142
|
+
resolve_reports() {
|
|
143
|
+
printf '%s\n' "$rec" | while IFS= read -r line; do
|
|
144
|
+
case "$line" in
|
|
145
|
+
"directReports:: "*) printf '%s' "${line#directReports:: }" | base64 -D 2>/dev/null; echo ;;
|
|
146
|
+
"directReports: "*) printf '%s\n' "${line#directReports: }" ;;
|
|
147
|
+
esac
|
|
148
|
+
done | while IFS= read -r dn; do
|
|
149
|
+
[ -n "$dn" ] || continue
|
|
150
|
+
n=$(dn_report "$dn"); [ -n "$n" ] && printf '%s\n' "$n"
|
|
151
|
+
done | sort
|
|
152
|
+
}
|
|
153
|
+
reports=$(resolve_reports)
|
|
65
154
|
|
|
66
|
-
# 6. Write the cache.
|
|
67
|
-
# plus extra fields.
|
|
155
|
+
# 6. Write the cache. It keeps the Name/Email/Domain shape other skills already
|
|
156
|
+
# parse, plus extra fields. The script generates it. Never edit it by hand.
|
|
68
157
|
mkdir -p "$(dirname "$CACHE")"
|
|
69
158
|
{
|
|
70
159
|
echo "# User Identity"
|
|
71
160
|
echo
|
|
72
|
-
echo "
|
|
73
|
-
echo "Do not edit by hand
|
|
161
|
+
echo "The \`person-identify\` skill generates this file from the corporate directory."
|
|
162
|
+
echo "Do not edit it by hand. Run the skill again to refresh it."
|
|
74
163
|
echo
|
|
75
164
|
echo "- **Name:** $name"
|
|
76
165
|
echo "- **Email:** $mail"
|
|
@@ -81,6 +170,16 @@ mkdir -p "$(dirname "$CACHE")"
|
|
|
81
170
|
echo "- **Employee ID:** $empid"
|
|
82
171
|
echo "- **Office:** $office"
|
|
83
172
|
[ -n "$mgr_name" ] && echo "- **Manager:** $mgr_name"
|
|
173
|
+
# Always emit Direct reports so the "our team" rule in CLAUDE.md can branch
|
|
174
|
+
# explicitly: a list => the user manages people (the team is in this cache);
|
|
175
|
+
# `none` => an individual contributor (peers resolved on demand from the
|
|
176
|
+
# manager). An omitted line would be indistinguishable from "not yet resolved".
|
|
177
|
+
if [ -n "$reports" ]; then
|
|
178
|
+
echo "- **Direct reports:**"
|
|
179
|
+
printf '%s\n' "$reports" | while IFS= read -r r; do echo " - $r"; done
|
|
180
|
+
else
|
|
181
|
+
echo "- **Direct reports:** none"
|
|
182
|
+
fi
|
|
84
183
|
} > "$CACHE"
|
|
85
184
|
|
|
86
185
|
# 7. Show the result.
|
|
@@ -1,37 +1,40 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: person-lookup
|
|
3
|
-
description: Look up ANY person in the corporate directory from free-text input (an email address, or a first / last / full name) and return their record
|
|
3
|
+
description: Look up ANY person in the corporate directory from free-text input (an email address, or a first / last / full name) and return their record: real name, title, department, company, email, employee ID, office, manager, and direct reports. Searches the Global Catalog forest-wide through LDAP. The bind uses the existing Kerberos ticket. Use when the user asks "who is X", needs someone's title / department / manager / reports / email, wants to disambiguate a name, or needs to resolve a team around a person (a person's peers = their manager's other reports). For the *current* user's own identity, use the sibling `person-identify` skill instead.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Person Lookup
|
|
7
7
|
|
|
8
|
+
Write tier: none (cache only)
|
|
9
|
+
Frontmatter: none
|
|
10
|
+
|
|
8
11
|
Resolve **any** person in the corporate Active Directory from natural-language
|
|
9
|
-
input
|
|
10
|
-
`person-identify`,
|
|
11
|
-
Catalog** (forest-wide)
|
|
12
|
-
|
|
13
|
-
cache. It
|
|
12
|
+
input. The input is an email, a login, or any part of a name. This skill is the
|
|
13
|
+
sibling of `person-identify`, and it aims at *other* people. It searches the
|
|
14
|
+
**Global Catalog** (forest-wide). It handles multiple matches. It flags
|
|
15
|
+
external contacts and vendor accounts. Unlike `person-identify`, it never
|
|
16
|
+
writes the identity cache. It is a read-only, throwaway lookup.
|
|
14
17
|
|
|
15
18
|
## Trigger
|
|
16
19
|
|
|
17
20
|
- The user asks "who is <name>", or for someone's title, department, manager,
|
|
18
21
|
email, or employee ID.
|
|
19
|
-
- A name is ambiguous and
|
|
22
|
+
- A name is ambiguous, and you must disambiguate it against the directory.
|
|
20
23
|
- Another skill needs to resolve a person who is **not** the current user.
|
|
21
24
|
|
|
22
|
-
For the current user's own record
|
|
23
|
-
|
|
25
|
+
For the current user's own record, use `person-identify` instead. It also
|
|
26
|
+
populates the identity cache that other skills read.
|
|
24
27
|
|
|
25
28
|
## Prerequisites
|
|
26
29
|
|
|
27
|
-
- A valid **Kerberos ticket** (`klist` shows a principal). If absent
|
|
30
|
+
- A valid **Kerberos ticket** (`klist` shows a principal). If absent, run
|
|
28
31
|
`kinit <user>@<REALM>`.
|
|
29
|
-
- Network
|
|
30
|
-
- `ldapsearch` and `dig` — both ship with macOS
|
|
32
|
+
- Network access to a domain controller (on-site or VPN).
|
|
33
|
+
- `ldapsearch` and `dig` — both ship with macOS. You install nothing.
|
|
31
34
|
|
|
32
|
-
Nothing is hardcoded
|
|
33
|
-
from the ticket and DNS. The bind uses SASL/GSSAPI against the existing
|
|
34
|
-
|
|
35
|
+
Nothing is hardcoded. The script derives the realm and a domain controller at
|
|
36
|
+
runtime from the ticket and DNS. The bind uses SASL/GSSAPI against the existing
|
|
37
|
+
ticket. You never enter a password.
|
|
35
38
|
|
|
36
39
|
## Usage
|
|
37
40
|
|
|
@@ -44,20 +47,21 @@ The argument is free text: an email, a full name, or just a surname.
|
|
|
44
47
|
|
|
45
48
|
## How it works
|
|
46
49
|
|
|
47
|
-
1. **Derive a domain controller** from the Kerberos principal's realm
|
|
48
|
-
SRV (same bootstrap as `person-identify`).
|
|
50
|
+
1. **Derive a domain controller** from the Kerberos principal's realm through
|
|
51
|
+
DNS SRV (same bootstrap as `person-identify`).
|
|
49
52
|
2. **Search the Global Catalog** (`ldap://$dc:3268`, base `""`). The GC spans
|
|
50
|
-
*every* domain in the forest
|
|
51
|
-
your own ticket
|
|
53
|
+
*every* domain in the forest. So you find a colleague in another region from
|
|
54
|
+
your own ticket. A plain domain-scoped search would miss them. It also
|
|
52
55
|
returns the common attributes (title, department, mail, manager), so one
|
|
53
56
|
query is enough.
|
|
54
|
-
3. **Match with ANR** (Ambiguous Name Resolution)
|
|
57
|
+
3. **Match with ANR** (Ambiguous Name Resolution). The `(anr=<input>)` filter
|
|
55
58
|
matches an email, a login, or any name part in one shot. If ANR finds
|
|
56
59
|
nothing, fall back to a substring search on `mail` / `displayName` /
|
|
57
60
|
`proxyAddresses`.
|
|
58
61
|
4. **Resolve per match.** For a single hit, print the full record and resolve
|
|
59
|
-
the `manager` DN
|
|
60
|
-
disambiguation list and suggest
|
|
62
|
+
both org edges — the `manager` DN and each `directReports` DN — to names.
|
|
63
|
+
For several hits, print a compact disambiguation list and suggest a
|
|
64
|
+
narrower search by email (org edges resolve only for an exact, single hit).
|
|
61
65
|
|
|
62
66
|
## Output
|
|
63
67
|
|
|
@@ -72,6 +76,7 @@ The argument is free text: an email, a full name, or just a surname.
|
|
|
72
76
|
| Employee ID | `employeeID` |
|
|
73
77
|
| Phone / Office | `telephoneNumber` / `physicalDeliveryOfficeName` |
|
|
74
78
|
| Manager | `manager` (DN → resolved to a name) |
|
|
79
|
+
| Direct reports | `directReports` (multi-valued DN back-link → each resolved to a name; single hit only, shown when non-empty) |
|
|
75
80
|
| DN | distinguished name (region + OU, useful for disambiguation) |
|
|
76
81
|
|
|
77
82
|
## Notes
|
|
@@ -80,18 +85,17 @@ The argument is free text: an email, a full name, or just a surname.
|
|
|
80
85
|
external **Contact** object (e.g. a vendor email alias), returns several
|
|
81
86
|
entries. The **Type** column distinguishes an internal **Employee** from an
|
|
82
87
|
external **Contact** (standard `objectClass=contact`). Some directories also
|
|
83
|
-
park external/vendor *user* accounts under a dedicated OU
|
|
88
|
+
park external/vendor *user* accounts under a dedicated OU. Set
|
|
84
89
|
`VENDOR_OU_PATTERN` at the top of `scripts/lookup.sh` to your directory's OU
|
|
85
|
-
substring (e.g. `OU=Contractors`)
|
|
86
|
-
since the OU convention is organization-specific. Narrow with an
|
|
87
|
-
exact hit.
|
|
90
|
+
substring (e.g. `OU=Contractors`). This flags those accounts too. It is empty
|
|
91
|
+
by default, since the OU convention is organization-specific. Narrow with an
|
|
92
|
+
email for an exact hit.
|
|
88
93
|
- **Silent partial results.** Under load the directory occasionally returns an
|
|
89
94
|
entry's DN with no attributes (exit 0, no error). Every attribute fetch
|
|
90
95
|
retries with backoff, so a throttled response never masquerades as a person
|
|
91
96
|
with a blank title or email.
|
|
92
97
|
- **No cache.** This skill prints and exits. It never touches
|
|
93
|
-
`~/.cache/fit/outpost/state/identity.md`
|
|
94
|
-
|
|
98
|
+
`~/.cache/fit/outpost/state/identity.md`. `person-identify` owns that file
|
|
99
|
+
alone.
|
|
95
100
|
- **Ethics.** This reads objective, work-relevant directory data only, in line
|
|
96
|
-
with the knowledge base's integrity rules.
|
|
97
|
-
dossiers.
|
|
101
|
+
with the knowledge base's integrity rules. Never use it to build dossiers.
|
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
# input (an email address, or a first / last / full name) and print their
|
|
4
4
|
# directory record(s).
|
|
5
5
|
#
|
|
6
|
-
# Sibling to `person-identify
|
|
6
|
+
# Sibling to `person-identify`. Use it for *other* people. Do not use it for
|
|
7
|
+
# the current user:
|
|
7
8
|
# - Searches the Global Catalog (port 3268, base "") so it reaches EVERY
|
|
8
|
-
# domain in the forest from a single ticket
|
|
9
|
-
#
|
|
9
|
+
# domain in the forest from a single ticket. It finds colleagues in other
|
|
10
|
+
# regions. It does not stop at your own domain.
|
|
10
11
|
# - Uses Active Directory ANR (Ambiguous Name Resolution), so one filter
|
|
11
12
|
# matches an email, a login, or any part of a name.
|
|
12
|
-
# - Handles 0, 1, or many matches
|
|
13
|
-
# accounts so an internal employee
|
|
14
|
-
# - NEVER writes the identity cache
|
|
13
|
+
# - Handles 0, 1, or many matches. Flags external Contacts / vendor
|
|
14
|
+
# accounts so you never confuse an internal employee with an outside one.
|
|
15
|
+
# - NEVER writes the identity cache. This is a throwaway lookup.
|
|
15
16
|
#
|
|
16
|
-
# Auth uses SASL/GSSAPI against the existing Kerberos ticket
|
|
17
|
+
# Auth uses SASL/GSSAPI against the existing Kerberos ticket. It needs no
|
|
18
|
+
# password.
|
|
17
19
|
#
|
|
18
20
|
# Robustness note: under load the directory intermittently returns an entry's
|
|
19
21
|
# DN with NO attributes (exit 0, no error). Every attribute fetch below retries
|
|
@@ -22,9 +24,10 @@
|
|
|
22
24
|
set -u
|
|
23
25
|
|
|
24
26
|
# Org-specific: substring of the DN's OU that marks external/vendor *user*
|
|
25
|
-
# accounts
|
|
26
|
-
# Empty by default
|
|
27
|
-
# or "OU=External", to label those accounts as
|
|
27
|
+
# accounts. The script detects external Contact objects generically through
|
|
28
|
+
# objectClass. Empty by default. Set it to your directory's convention, e.g.
|
|
29
|
+
# "OU=Contractors" or "OU=External", to label those accounts as
|
|
30
|
+
# "User (external / vendor)".
|
|
28
31
|
VENDOR_OU_PATTERN=""
|
|
29
32
|
|
|
30
33
|
QUERY="$*"
|
|
@@ -44,16 +47,16 @@ dom=$(printf '%s' "$realm" | tr '[:upper:]' '[:lower:]')
|
|
|
44
47
|
dc=$(dig +short SRV "_ldap._tcp.dc._msdcs.$dom" | awk 'NR==1{print $4}' | sed 's/\.$//')
|
|
45
48
|
[ -z "$dc" ] && dc=$(dig +short SRV "_ldap._tcp.$dom" | awk 'NR==1{print $4}' | sed 's/\.$//')
|
|
46
49
|
if [ -z "$dc" ]; then
|
|
47
|
-
echo "Could not find a domain controller for $dom
|
|
50
|
+
echo "Could not find a domain controller for $dom with DNS SRV." >&2
|
|
48
51
|
exit 1
|
|
49
52
|
fi
|
|
50
53
|
|
|
51
54
|
# Global Catalog: forest-wide, base "" spans every domain.
|
|
52
55
|
GC="ldap://$dc:3268"
|
|
53
|
-
ATTRS="displayName givenName sn company title department employeeID mail telephoneNumber physicalDeliveryOfficeName objectClass manager"
|
|
56
|
+
ATTRS="displayName givenName sn company title department employeeID mail telephoneNumber physicalDeliveryOfficeName objectClass manager directReports"
|
|
54
57
|
MAX_SHOW=12 # cap detailed output for very broad name matches
|
|
55
58
|
|
|
56
|
-
# Pull one attribute out of an LDIF record (passed as $2)
|
|
59
|
+
# Pull one attribute out of an LDIF record (passed as $2). Decode base64 (::).
|
|
57
60
|
field() { # $1=attr $2=record
|
|
58
61
|
local attr="$1" line
|
|
59
62
|
line=$(printf '%s\n' "$2" | grep -m1 -E "^$attr:: ?|^$attr: ") || return 0
|
|
@@ -63,13 +66,13 @@ field() { # $1=attr $2=record
|
|
|
63
66
|
esac
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
# List
|
|
69
|
+
# List the DNs that match a filter. Request no attributes → reliable under load.
|
|
67
70
|
dns_for() { # $1=filter
|
|
68
71
|
ldapsearch -Y GSSAPI -LLL -o ldif-wrap=no -H "$GC" -b "" "$1" 1.1 2>/dev/null \
|
|
69
72
|
| sed -n 's/^dn: //p'
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
# Fetch one entry's attributes by DN
|
|
75
|
+
# Fetch one entry's attributes by DN. Retry past the silent DN-only response.
|
|
73
76
|
fetch() { # $1=dn
|
|
74
77
|
local dn="$1" out tries=0
|
|
75
78
|
while [ "$tries" -lt 4 ]; do
|
|
@@ -87,6 +90,23 @@ name_of_dn() { # $1=dn
|
|
|
87
90
|
field displayName "$(fetch "$1")"
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
# List a person's direct reports — the multi-valued `directReports` back-link
|
|
94
|
+
# on their record — as one resolved name per line, sorted. $1 = the person's
|
|
95
|
+
# fetched LDIF record. field() keeps only the first value, so pull EVERY value
|
|
96
|
+
# here, decoding the base64 `directReports:: <b64>` form. Kept in a function
|
|
97
|
+
# because a `case` inside $(...) trips the bash 3.2 parser that macOS ships.
|
|
98
|
+
reports_of() { # $1=record
|
|
99
|
+
printf '%s\n' "$1" | while IFS= read -r line; do
|
|
100
|
+
case "$line" in
|
|
101
|
+
"directReports:: "*) printf '%s' "${line#directReports:: }" | base64 -D 2>/dev/null; echo ;;
|
|
102
|
+
"directReports: "*) printf '%s\n' "${line#directReports: }" ;;
|
|
103
|
+
esac
|
|
104
|
+
done | while IFS= read -r dn; do
|
|
105
|
+
[ -n "$dn" ] || continue
|
|
106
|
+
n=$(name_of_dn "$dn"); [ -n "$n" ] && printf '%s\n' "$n"
|
|
107
|
+
done | sort
|
|
108
|
+
}
|
|
109
|
+
|
|
90
110
|
# Classify an entry: internal employee, external contact, or vendor account.
|
|
91
111
|
kind_of() { # $1=record $2=dn
|
|
92
112
|
local oc; oc=$(printf '%s\n' "$1" | grep -i '^objectClass:' | tr 'A-Z' 'a-z')
|
|
@@ -100,7 +120,7 @@ kind_of() { # $1=record $2=dn
|
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
# 2. Primary search (ANR), then a substring fallback if it finds nothing.
|
|
103
|
-
# (Portable array fill
|
|
123
|
+
# (Portable array fill. macOS ships bash 3.2, which has no `mapfile`.)
|
|
104
124
|
read_dns() { # $1=filter -> populates global DNS array
|
|
105
125
|
DNS=()
|
|
106
126
|
local line
|
|
@@ -133,12 +153,21 @@ if [ "${#DNS[@]}" -eq 1 ]; then
|
|
|
133
153
|
echo "- **Phone:** $(field telephoneNumber "$rec")"
|
|
134
154
|
echo "- **Office:** $(field physicalDeliveryOfficeName "$rec")"
|
|
135
155
|
[ -n "$mgr" ] && echo "- **Manager:** $mgr"
|
|
156
|
+
# Direct reports — the down-edge, symmetric to person-identify. Lets a
|
|
157
|
+
# caller resolve a team from any person: an IC's peers are their manager's
|
|
158
|
+
# other reports (look up the manager, read this list). Printed only when
|
|
159
|
+
# non-empty.
|
|
160
|
+
reports=$(reports_of "$rec")
|
|
161
|
+
if [ -n "$reports" ]; then
|
|
162
|
+
echo "- **Direct reports:**"
|
|
163
|
+
printf '%s\n' "$reports" | while IFS= read -r r; do echo " - $r"; done
|
|
164
|
+
fi
|
|
136
165
|
echo "- **DN:** ${DNS[0]}"
|
|
137
166
|
exit 0
|
|
138
167
|
fi
|
|
139
168
|
|
|
140
169
|
# 3b. Several matches → a compact disambiguation list (no per-row manager call).
|
|
141
|
-
echo "${#DNS[@]} matches for \"$QUERY\"
|
|
170
|
+
echo "${#DNS[@]} matches for \"$QUERY\". Narrow with an email for an exact hit:"
|
|
142
171
|
echo
|
|
143
172
|
shown=0
|
|
144
173
|
for dn in "${DNS[@]}"; do
|