geekbot-cli 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +517 -0
  3. package/package.json +50 -0
  4. package/scripts/postinstall.mjs +27 -0
  5. package/skills/geekbot/SKILL.md +281 -0
  6. package/skills/geekbot/check-cli.sh +36 -0
  7. package/skills/geekbot/cli-commands.md +382 -0
  8. package/skills/geekbot/error-recovery.md +95 -0
  9. package/skills/geekbot/manager-workflows.md +408 -0
  10. package/skills/geekbot/reporter-workflows.md +275 -0
  11. package/skills/geekbot/standup-templates.json +244 -0
  12. package/src/auth/keychain.ts +38 -0
  13. package/src/auth/resolver.ts +44 -0
  14. package/src/cli/commands/auth.ts +56 -0
  15. package/src/cli/commands/me.ts +34 -0
  16. package/src/cli/commands/poll.ts +91 -0
  17. package/src/cli/commands/report.ts +66 -0
  18. package/src/cli/commands/standup.ts +234 -0
  19. package/src/cli/commands/team.ts +40 -0
  20. package/src/cli/globals.ts +31 -0
  21. package/src/cli/index.ts +94 -0
  22. package/src/errors/cli-error.ts +16 -0
  23. package/src/errors/error-handler.ts +63 -0
  24. package/src/errors/exit-codes.ts +14 -0
  25. package/src/errors/not-found-helper.ts +86 -0
  26. package/src/handlers/auth-handlers.ts +152 -0
  27. package/src/handlers/me-handlers.ts +27 -0
  28. package/src/handlers/poll-handlers.ts +187 -0
  29. package/src/handlers/report-handlers.ts +87 -0
  30. package/src/handlers/standup-handlers.ts +534 -0
  31. package/src/handlers/team-handlers.ts +38 -0
  32. package/src/http/authenticated-client.ts +17 -0
  33. package/src/http/client.ts +138 -0
  34. package/src/http/errors.ts +134 -0
  35. package/src/output/envelope.ts +50 -0
  36. package/src/output/formatter.ts +12 -0
  37. package/src/schemas/common.ts +13 -0
  38. package/src/schemas/poll.ts +89 -0
  39. package/src/schemas/report.ts +124 -0
  40. package/src/schemas/standup.ts +64 -0
  41. package/src/schemas/team.ts +11 -0
  42. package/src/schemas/user.ts +70 -0
  43. package/src/types.ts +30 -0
  44. package/src/utils/constants.ts +24 -0
  45. package/src/utils/input-parsers.ts +234 -0
  46. package/src/utils/receipt.ts +94 -0
  47. package/src/utils/validation.ts +128 -0
@@ -0,0 +1,275 @@
1
+ # Reporter Workflows
2
+
3
+ Detailed guide for the AI-assisted reporting experience. The SKILL.md
4
+ covers the 5-step pipeline at a high level; this document covers the
5
+ full flow including tone calibration, blocker carry-over, and edge cases.
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Report Drafting Pipeline (detailed)](#report-drafting-pipeline)
10
+ 2. [Tone Calibration](#tone-calibration)
11
+ 3. [Blocker Carry-Over Logic](#blocker-carry-over-logic)
12
+ 4. [Edge Cases](#edge-cases)
13
+
14
+ ---
15
+
16
+ ## Report Drafting Pipeline
17
+
18
+ ### Step 1: Identify the standup
19
+
20
+ Run `geekbot standup list` to get the user's standups.
21
+
22
+ - **Single standup**: Use it automatically. Mention which one: "I see you're
23
+ in the 'Daily Standup' — let's draft your report for that."
24
+ - **Multiple standups**: Present a short list (ID, name, channel) and ask
25
+ which one. If the user's request hints at one ("my daily"), match on name.
26
+ - **No standups**: The user might not be a participant in any standup.
27
+ Suggest they check with their manager or run `geekbot standup list --admin`
28
+ if they're an admin.
29
+
30
+ ### Step 2: Fetch questions
31
+
32
+ ```bash
33
+ geekbot standup get <standup_id>
34
+ ```
35
+
36
+ Extract from the response:
37
+ - `questions[].id` — you need these for the report submission
38
+ - `questions[].text` — show these to the user and use as draft structure
39
+
40
+ Store the question ID ↔ text mapping for use in step 5.
41
+
42
+ ### Step 3: Gather context
43
+
44
+ Context comes from three sources. Check them in this order — each one makes
45
+ the draft richer.
46
+
47
+ #### Source A: Connected MCP servers (opportunistic enrichment)
48
+
49
+ Check what MCP servers are available in the current session. Use whatever
50
+ is connected to pull the user's recent activity. This is what transforms
51
+ "help me write my standup" from a Q&A session into a one-click draft.
52
+
53
+ The key concept: map MCP entities to standup questions — what they *did*,
54
+ what they *will do*, and what's *blocking* them.
55
+
56
+ | Source | Entity | Maps to |
57
+ |--------|--------|---------|
58
+ | GitHub/GitLab | Merged PRs, closed issues, review comments | "What did you do?" |
59
+ | GitHub/GitLab | Open/draft PRs (authored) | "What will you do?" |
60
+ | GitHub/GitLab | PRs with changes requested | "Blockers?" |
61
+ | Jira/Linear/Asana | Tickets moved to Done | "What did you do?" |
62
+ | Jira/Linear/Asana | In Progress tickets | "What will you do?" |
63
+ | Jira/Linear/Asana | Blocked/On Hold tickets | "Blockers?" |
64
+ | Calendar | Meetings attended since last report | "What did you do?" |
65
+ | Calendar | Upcoming meetings today | "What will you do?" |
66
+ | Slack | Threads participated in, announcements | Any question |
67
+
68
+ Filter calendar noise: skip the standup itself, recurring 1:1s, and
69
+ all-day events unless meaningful (offsites, deadlines).
70
+
71
+ **Deduplication:** Group related entities across tools — "Merged PR #342,
72
+ closes PROJ-89" is better than listing each separately.
73
+
74
+ **Identity matching:** MCP servers use different identifiers (Slack IDs,
75
+ GitHub usernames, emails). If they don't match, ask the user once and
76
+ reuse for the conversation.
77
+
78
+ **Enrichment flow:**
79
+ 1. Silently check which MCP servers are connected
80
+ 2. Pull recent activity from each (since last report date)
81
+ 3. Deduplicate across tools, group by standup question
82
+ 4. Present: "I pulled your recent activity — here's what I found"
83
+
84
+ If no MCP servers are connected, skip silently. Don't mention missing
85
+ access unprompted — explain only if the user asks why the draft isn't
86
+ auto-populated.
87
+
88
+ #### Source B: Previous Geekbot reports (always available)
89
+
90
+ ```bash
91
+ geekbot report list --standup-id <id> --user-id <uid> --limit 5
92
+ ```
93
+
94
+ Where `<uid>` comes from `geekbot me show` → `data.id`. Extract: typical
95
+ answer length, writing style (bullets vs prose), recurring themes, and
96
+ any unresolved blockers for carry-over.
97
+
98
+ #### Source C: The user's direct input
99
+
100
+ **With MCP context:** Show what you found, ask "Anything to add, correct,
101
+ or remove?" — the user validates instead of recalling from scratch.
102
+
103
+ **Without MCP:** This is the primary source. Let the user dump context
104
+ freely ("What have you been working on?") — you structure it into answers.
105
+
106
+ #### Combining sources
107
+
108
+ Merge priority: user corrections > MCP specifics (PR numbers, ticket IDs) >
109
+ previous reports (style/tone) > user narrative input (the "why").
110
+
111
+ ### Step 4: Draft answers
112
+
113
+ For each question in the standup, draft an answer that:
114
+
115
+ 1. **Addresses the question directly** — don't go off-topic
116
+ 2. **Matches the user's historical style** (see Tone Calibration below)
117
+ 3. **Leads with specifics from MCP data** — PR numbers, ticket IDs,
118
+ meeting names add credibility and save the user from remembering details
119
+ 4. **Incorporates their direct input** — the user's words provide narrative
120
+ and context that tools can't capture
121
+ 5. **Includes blocker carry-over** — surface unresolved blockers from
122
+ previous reports (see Blocker Carry-Over below)
123
+
124
+ **Example with enrichment:**
125
+ ```
126
+ Q: What have you done since yesterday?
127
+ A: Merged PR #342 (auth module refactor), closed PROJ-89 (payment webhook
128
+ bug). Had a sync with the API team about token rotation strategy.
129
+
130
+ Q: What will you do today?
131
+ A: Starting on the new billing API (PROJ-102). PR #345 is waiting for
132
+ review from @alice.
133
+
134
+ Q: Any blockers?
135
+ A: Still waiting on the staging deploy from DevOps (carrying over from
136
+ Tuesday). Also blocked on PROJ-98 — needs design sign-off.
137
+ ```
138
+
139
+ vs. **without enrichment (user input only):**
140
+ ```
141
+ Q: What have you done since yesterday?
142
+ A: Fixed the payment webhook bug and worked on the auth refactor.
143
+ ```
144
+
145
+ The enriched version is more useful for the team reading the report.
146
+
147
+ If the user didn't provide enough context for a specific question, ask
148
+ about that question specifically rather than inventing content.
149
+
150
+ ### Step 5: Review and post
151
+
152
+ Present the complete draft clearly:
153
+
154
+ ```
155
+ Here's your draft for "Daily Standup":
156
+
157
+ Q: What did you work on yesterday?
158
+ A: Finished the auth module refactor and opened PR #342 for review.
159
+
160
+ Q: What are you working on today?
161
+ A: Starting integration tests for the new auth flow. Meeting with
162
+ the API team at 14:00 to discuss token rotation.
163
+
164
+ Q: Any blockers?
165
+ A: Still waiting on the staging environment deploy from DevOps
166
+ (carried over from Tuesday).
167
+
168
+ Ready to submit?
169
+ ```
170
+
171
+ On explicit approval, build and execute:
172
+
173
+ ```bash
174
+ geekbot report create \
175
+ --standup-id 123 \
176
+ --answers '{"101":"Finished the auth module refactor and opened PR #342 for review.","102":"Starting integration tests for the new auth flow. Meeting with the API team at 14:00 to discuss token rotation.","103":"Still waiting on the staging environment deploy from DevOps (carried over from Tuesday)."}'
177
+ ```
178
+
179
+ Confirm success with the report ID and a brief summary.
180
+
181
+ ---
182
+
183
+ ## Tone Calibration
184
+
185
+ Match the user's existing reporting style. This matters — people notice
186
+ when their standup answers suddenly sound different.
187
+
188
+ ### What to analyse from historical reports
189
+
190
+ | Signal | How to detect | How to match |
191
+ |--------|--------------|--------------|
192
+ | **Length** | Average character count per answer | Keep within ±20% of their norm |
193
+ | **Structure** | Bullets vs prose vs numbered lists | Mirror the format |
194
+ | **Formality** | "Completed authentication module" vs "wrapped up auth stuff" | Match register |
195
+ | **Detail level** | PR numbers, ticket refs vs high-level summaries | Include same specificity |
196
+ | **Emoji / voice** | Present or absent; "I did X" vs "Did X" vs "Completed X" | Mirror exactly |
197
+
198
+ **If terse bullet points historically** → draft terse bullets, don't expand.
199
+ **If narrative with context** → draft with similar detail and flow.
200
+ **First-time reporter** → default to concise prose, 1–3 sentences per question.
201
+
202
+ ---
203
+
204
+ ## Blocker Carry-Over Logic
205
+
206
+ One of the most valuable parts of AI-assisted reporting: automatically
207
+ surfacing unresolved blockers so they don't silently disappear.
208
+
209
+ ### Detection
210
+
211
+ Scan last 3–5 reports for blocker-related questions (keywords: "block",
212
+ "stuck", "impediment", "waiting", "depend"). Classify answers as **no
213
+ blocker** ("None", "All clear", "N/A", short negatives) or **has blocker**
214
+ (anything else).
215
+
216
+ ### Resolution detection
217
+
218
+ - Later report's blocker answer is "None" or similar → resolved
219
+ - Later report mentions same topic in progress answer → resolved
220
+ - Same blocker in most recent report → still active
221
+
222
+ ### Carry-over presentation
223
+
224
+ Surface active blockers during step 4: *"In your last report (Tuesday),
225
+ you mentioned waiting on the staging deploy. Is that still blocking you?"*
226
+
227
+ - **Still blocked** → include, note it's a carry-over
228
+ - **Resolved** → omit (optionally mention resolution in progress answer)
229
+ - **Changed** → update the description
230
+
231
+ ### Don't over-carry
232
+
233
+ Only carry over from last 3–5 reports. If a blocker persists 2+ weeks,
234
+ still surface but don't belabour it. If the user says "no blockers" after
235
+ you surface a carry-over, respect that.
236
+
237
+ ---
238
+
239
+ ## Edge Cases
240
+
241
+ ### User in multiple standups with overlapping questions
242
+
243
+ If multiple standups have similar questions (common with "blockers" across
244
+ a daily standup and a weekly sync), draft each report independently. A
245
+ blocker reported in one standup should be surfaced for the other if it's
246
+ the same blocker — but the framing may differ (daily: tactical, weekly:
247
+ strategic).
248
+
249
+ ### Standup with many questions (>5)
250
+
251
+ Some standups (e.g., Sales Report, Well-being Check-in) have 5+ questions.
252
+ Don't try to gather context for all at once. Process in batches:
253
+ 1. Draft the first 3 answers based on initial context
254
+ 2. Ask about the remaining questions specifically
255
+ 3. Complete the draft
256
+
257
+ ### User provides minimal input
258
+
259
+ If the user says something like "nothing much today" or "same as yesterday":
260
+ - For progress questions: pull from the last report and frame as continuation
261
+ ("Still working on the auth refactor from yesterday?")
262
+ - For blocker questions: check carry-over, default to "No blockers" if clear
263
+ - Don't pad — if they want a short report, draft a short report
264
+
265
+ ### Report already submitted
266
+
267
+ If the user tries to submit a report and the CLI returns a conflict (exit
268
+ code 8), they may have already reported today. Inform them and ask if they
269
+ want to view their existing report instead.
270
+
271
+ ### Standup is inactive or paused
272
+
273
+ If `geekbot standup get` shows the standup isn't currently active, let the
274
+ user know. They can still submit a report manually, but the standup won't
275
+ trigger automatically until reactivated.
@@ -0,0 +1,244 @@
1
+ {
2
+ "standup_templates": [
3
+ {
4
+ "id": "daily-standup",
5
+ "name": "Daily Standup",
6
+ "description": "Track daily updates and blockers on autopilot. The classic async standup for engineering and general teams.",
7
+ "categories": ["daily-syncs", "team-updates", "remote-collaboration"],
8
+ "roles": ["team-leads", "project-managers", "product-managers", "agile-team-members"],
9
+ "questions": [
10
+ { "text": "What have you done since yesterday?" },
11
+ { "text": "What will you do today?" },
12
+ { "text": "Anything blocking your progress?" },
13
+ { "text": "How do you feel today?" }
14
+ ],
15
+ "defaults": {
16
+ "days": "Mon,Tue,Wed,Thu,Fri",
17
+ "time": "10:00"
18
+ }
19
+ },
20
+ {
21
+ "id": "retrospective",
22
+ "name": "Retrospective",
23
+ "description": "Reflect on what went well and what could go better. Designed for sprint or project retrospectives.",
24
+ "categories": ["continuous-improvement", "remote-collaboration"],
25
+ "roles": ["agile-team-members", "product-managers", "team-leads"],
26
+ "questions": [
27
+ { "text": "What went well?" },
28
+ { "text": "What didn't go so well?" },
29
+ { "text": "What have you learned?" },
30
+ { "text": "What still puzzles you?" }
31
+ ],
32
+ "defaults": {
33
+ "days": "Fri",
34
+ "time": "16:30",
35
+ "note": "Typically bi-weekly, aligned with sprint cadence"
36
+ }
37
+ },
38
+ {
39
+ "id": "sales-report",
40
+ "name": "Sales Report",
41
+ "description": "Report sales progress and track team performance. End-of-day sales metrics and pipeline updates.",
42
+ "categories": ["daily-syncs", "team-updates"],
43
+ "roles": ["team-leads"],
44
+ "questions": [
45
+ { "text": "How many meetings did you setup today?" },
46
+ { "text": "How many client conversations did you have today?" },
47
+ { "text": "How many new leads did you create today?" },
48
+ { "text": "Anything you'd like to add?" },
49
+ { "text": "Anything blocking your progress?" }
50
+ ],
51
+ "defaults": {
52
+ "days": "Mon,Tue,Wed,Thu,Fri",
53
+ "time": "16:30"
54
+ }
55
+ },
56
+ {
57
+ "id": "quick-check-in",
58
+ "name": "Quick Check-in",
59
+ "description": "Keep everyone in the loop with 3 quick questions. Lighter than a full standup, good for small teams.",
60
+ "categories": ["daily-syncs", "team-updates", "remote-collaboration"],
61
+ "roles": ["team-leads", "project-managers"],
62
+ "questions": [
63
+ { "text": "What's your main focus today?" },
64
+ { "text": "How are you feeling?" },
65
+ { "text": "Anything blocking your progress?" }
66
+ ],
67
+ "defaults": {
68
+ "days": "Mon,Tue,Wed,Thu,Fri",
69
+ "time": "09:00"
70
+ }
71
+ },
72
+ {
73
+ "id": "well-being-check-in",
74
+ "name": "Well-being Check-in",
75
+ "description": "Check in on how your team is really doing. Covers productivity, stress, and satisfaction. Best run anonymously.",
76
+ "categories": ["team-well-being", "remote-collaboration"],
77
+ "roles": ["people-managers"],
78
+ "questions": [
79
+ { "text": "Thinking about this past week: how productive do you feel you were?" },
80
+ { "text": "Do you feel you've had more wins or roadblocks this past week?" },
81
+ { "text": "How stressful did you find work in general this week?" },
82
+ { "text": "How satisfied do you feel about the work you've completed this week?" },
83
+ { "text": "Do you feel satisfied with your work-life balance?" },
84
+ { "text": "Overall, how do you feel about your level of work-related stress?" },
85
+ { "text": "Do you feel you can discuss concerns with your manager as often as needed?" }
86
+ ],
87
+ "defaults": {
88
+ "days": "Fri",
89
+ "time": "16:00",
90
+ "note": "Typically bi-weekly, anonymous"
91
+ }
92
+ },
93
+ {
94
+ "id": "confidential-check-in",
95
+ "name": "Confidential Check-in",
96
+ "description": "Run multiple 1-on-1s with your team all in one go. Replies stay private between manager and each participant.",
97
+ "categories": ["team-updates", "remote-collaboration"],
98
+ "roles": ["project-managers", "team-leads"],
99
+ "questions": [
100
+ { "text": "Was anything a particular struggle?" },
101
+ { "text": "If there was one thing I could do to help you, what would it be?" },
102
+ { "text": "On a scale of 1-10, how happy are you with your work-life balance?" },
103
+ { "text": "Growth-wise, where do you want to focus next month?" },
104
+ { "text": "Anything you'd like to add?" }
105
+ ],
106
+ "defaults": {
107
+ "days": "Fri",
108
+ "time": "15:00",
109
+ "note": "Weekly or bi-weekly, private channel between manager and participant"
110
+ }
111
+ },
112
+ {
113
+ "id": "meeting-notes",
114
+ "name": "Meeting Notes",
115
+ "description": "Sum up the talking points of every meeting. Structured capture of decisions and action items.",
116
+ "categories": ["team-updates"],
117
+ "roles": ["project-managers", "product-managers"],
118
+ "questions": [
119
+ { "text": "What were the main talking points?" },
120
+ { "text": "What decisions were made?" },
121
+ { "text": "What are the action items and who owns them?" }
122
+ ],
123
+ "defaults": {
124
+ "days": "Mon,Tue,Wed,Thu,Fri",
125
+ "time": "17:00",
126
+ "note": "Trigger on-demand after meetings rather than on a fixed schedule"
127
+ }
128
+ },
129
+ {
130
+ "id": "today-i-learned",
131
+ "name": "Today I Learned",
132
+ "description": "Inspire your team to learn something new. A lightweight knowledge-sharing ritual.",
133
+ "categories": ["continuous-improvement", "team-bonding"],
134
+ "roles": ["team-leads"],
135
+ "questions": [
136
+ { "text": "What did you learn today?" },
137
+ { "text": "How might this be useful for the team?" }
138
+ ],
139
+ "defaults": {
140
+ "days": "Mon,Tue,Wed,Thu,Fri",
141
+ "time": "17:00"
142
+ }
143
+ },
144
+ {
145
+ "id": "product-changelog",
146
+ "name": "Product Changelog",
147
+ "description": "Keep track of changes in product development. Structured release and change tracking.",
148
+ "categories": ["team-updates", "continuous-improvement"],
149
+ "roles": ["product-managers"],
150
+ "questions": [
151
+ { "text": "What changed or shipped today?" },
152
+ { "text": "What's the user impact?" },
153
+ { "text": "Any follow-up items or known issues?" }
154
+ ],
155
+ "defaults": {
156
+ "days": "Mon,Tue,Wed,Thu,Fri",
157
+ "time": "17:00"
158
+ }
159
+ },
160
+ {
161
+ "id": "incident-log",
162
+ "name": "Incident Log",
163
+ "description": "Log incidents to share with the team. Structured incident reporting and post-mortem capture.",
164
+ "categories": ["team-updates"],
165
+ "roles": ["team-leads"],
166
+ "questions": [
167
+ { "text": "What happened?" },
168
+ { "text": "What was the impact?" },
169
+ { "text": "What was the root cause?" },
170
+ { "text": "What are the follow-up actions?" }
171
+ ],
172
+ "defaults": {
173
+ "days": "Mon,Tue,Wed,Thu,Fri",
174
+ "time": "09:00",
175
+ "note": "Trigger on-demand when incidents occur"
176
+ }
177
+ },
178
+ {
179
+ "id": "monday-watercooler",
180
+ "name": "Monday Water Cooler",
181
+ "description": "Spark Monday morning chitchat with a friendly question. Team bonding for remote teams.",
182
+ "categories": ["team-bonding"],
183
+ "roles": ["team-leads", "people-managers"],
184
+ "questions": [
185
+ { "text": "What was the highlight of your weekend?" },
186
+ { "text": "What are you looking forward to this week?" }
187
+ ],
188
+ "defaults": {
189
+ "days": "Mon",
190
+ "time": "09:30"
191
+ }
192
+ },
193
+ {
194
+ "id": "team-feedback",
195
+ "name": "Team Feedback",
196
+ "description": "Let your teammates know how they're doing. Structured peer feedback collection.",
197
+ "categories": ["continuous-improvement", "team-bonding"],
198
+ "roles": ["team-leads", "people-managers"],
199
+ "questions": [
200
+ { "text": "Who on the team did great work recently and what did they do?" },
201
+ { "text": "What's one thing we could do better as a team?" },
202
+ { "text": "Any shout-outs or kudos?" }
203
+ ],
204
+ "defaults": {
205
+ "days": "Fri",
206
+ "time": "16:00"
207
+ }
208
+ }
209
+ ],
210
+ "poll_templates": [
211
+ {
212
+ "id": "yes-no",
213
+ "name": "Yes / No",
214
+ "description": "Make fast decisions with simple Yes/No votes.",
215
+ "question": "Do you agree with the proposal?",
216
+ "choices": ["Yes", "No"],
217
+ "note": "Replace the question with your actual decision topic"
218
+ },
219
+ {
220
+ "id": "agree-disagree",
221
+ "name": "Agree / Disagree",
222
+ "description": "Quickly gauge sentiment on a topic with a Likert-style scale.",
223
+ "question": "How do you feel about this proposal?",
224
+ "choices": ["Strongly Agree", "Agree", "Neutral", "Disagree", "Strongly Disagree"],
225
+ "note": "Replace the question with your actual topic"
226
+ },
227
+ {
228
+ "id": "employee-nps",
229
+ "name": "Employee NPS Survey",
230
+ "description": "Evaluate and track employee commitment and loyalty with a standard NPS question.",
231
+ "question": "On a scale of 0-10, how likely are you to recommend this company as a place to work?",
232
+ "choices": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
233
+ "note": "NPS = % Promoters (9-10) minus % Detractors (0-6)"
234
+ },
235
+ {
236
+ "id": "team-feedback-weekly",
237
+ "name": "Team Feedback Weekly",
238
+ "description": "Gather weekly insights on team performance and morale.",
239
+ "question": "How would you rate this week's team performance?",
240
+ "choices": ["Excellent", "Good", "Average", "Below Average", "Poor"],
241
+ "note": "Good for tracking team health trends over time"
242
+ }
243
+ ]
244
+ }
@@ -0,0 +1,38 @@
1
+ import { Entry } from "@napi-rs/keyring";
2
+
3
+ const SERVICE = "geekbot-cli";
4
+ const ACCOUNT = "api-key";
5
+
6
+ /**
7
+ * Retrieve API key from OS keychain.
8
+ * Returns null if keychain is unavailable or no entry exists.
9
+ */
10
+ export function getKeychainKey(): string | null {
11
+ try {
12
+ const entry = new Entry(SERVICE, ACCOUNT);
13
+ return entry.getPassword();
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Store API key in OS keychain.
21
+ * Throws if keychain is unavailable (caller should catch and suggest env var).
22
+ */
23
+ export function setKeychainKey(apiKey: string): void {
24
+ if (!apiKey.trim()) {
25
+ throw new Error("API key must not be empty");
26
+ }
27
+ const entry = new Entry(SERVICE, ACCOUNT);
28
+ entry.setPassword(apiKey);
29
+ }
30
+
31
+ /**
32
+ * Delete API key from OS keychain.
33
+ * Throws if keychain is unavailable or no entry exists.
34
+ */
35
+ export function deleteKeychainKey(): void {
36
+ const entry = new Entry(SERVICE, ACCOUNT);
37
+ entry.deletePassword();
38
+ }
@@ -0,0 +1,44 @@
1
+ import { CliError } from "../errors/cli-error.ts";
2
+ import { ExitCode } from "../errors/exit-codes.ts";
3
+ import { getKeychainKey as _getKeychainKey } from "./keychain.ts";
4
+
5
+ export interface CredentialResult {
6
+ apiKey: string;
7
+ source: "flag" | "env" | "keychain";
8
+ }
9
+
10
+ export async function resolveCredential(
11
+ options: { apiKeyFlag?: string },
12
+ getKeychainKeyImpl?: typeof _getKeychainKey,
13
+ ): Promise<CredentialResult> {
14
+ // Priority 1: --api-key flag
15
+ if (options.apiKeyFlag) {
16
+ return { apiKey: options.apiKeyFlag.trim(), source: "flag" };
17
+ }
18
+
19
+ // Priority 2: GEEKBOT_API_KEY env var
20
+ const envKey = process.env.GEEKBOT_API_KEY;
21
+ if (envKey) {
22
+ return { apiKey: envKey.trim(), source: "env" };
23
+ }
24
+
25
+ // Priority 3: OS keychain
26
+ const getKey = getKeychainKeyImpl ?? _getKeychainKey;
27
+ try {
28
+ const keychainKey = getKey();
29
+ if (keychainKey) {
30
+ return { apiKey: keychainKey.trim(), source: "keychain" };
31
+ }
32
+ } catch {
33
+ // Keychain unavailable (headless, CI) -- fall through to error
34
+ }
35
+
36
+ // No credential found -- list all sources checked
37
+ throw new CliError(
38
+ "No API key found. Checked: --api-key flag, GEEKBOT_API_KEY environment variable, OS keychain.",
39
+ "auth_missing",
40
+ ExitCode.AUTH,
41
+ false,
42
+ "Set GEEKBOT_API_KEY environment variable or run: geekbot auth setup",
43
+ );
44
+ }
@@ -0,0 +1,56 @@
1
+ import { Command } from "commander";
2
+ import { handleError } from "../../errors/error-handler.ts";
3
+ import {
4
+ handleAuthRemove,
5
+ handleAuthSetup,
6
+ handleAuthStatus,
7
+ } from "../../handlers/auth-handlers.ts";
8
+ import { getGlobalOptions } from "../globals.ts";
9
+
10
+ export function createAuthCommand(): Command {
11
+ const auth = new Command("auth").description("Manage authentication");
12
+
13
+ auth
14
+ .command("setup")
15
+ .description("Interactively configure and store API key")
16
+ .addHelpText(
17
+ "after",
18
+ "\nExamples:\n geekbot auth setup\n geekbot --api-key YOUR_KEY auth setup",
19
+ )
20
+ .action(async function (this: Command) {
21
+ const globalOpts = getGlobalOptions(this);
22
+ try {
23
+ await handleAuthSetup({ apiKey: globalOpts.apiKey }, globalOpts);
24
+ } catch (error) {
25
+ handleError(error, globalOpts.debug);
26
+ }
27
+ });
28
+
29
+ auth
30
+ .command("status")
31
+ .description("Verify stored credentials work")
32
+ .addHelpText("after", "\nExamples:\n geekbot auth status")
33
+ .action(async function (this: Command) {
34
+ const globalOpts = getGlobalOptions(this);
35
+ try {
36
+ await handleAuthStatus(globalOpts);
37
+ } catch (error) {
38
+ handleError(error, globalOpts.debug);
39
+ }
40
+ });
41
+
42
+ auth
43
+ .command("remove")
44
+ .description("Remove stored API key from OS keychain")
45
+ .addHelpText("after", "\nExamples:\n geekbot auth remove")
46
+ .action(async function (this: Command) {
47
+ const globalOpts = getGlobalOptions(this);
48
+ try {
49
+ await handleAuthRemove(globalOpts);
50
+ } catch (error) {
51
+ handleError(error, globalOpts.debug);
52
+ }
53
+ });
54
+
55
+ return auth;
56
+ }
@@ -0,0 +1,34 @@
1
+ import { Command } from "commander";
2
+ import { handleError } from "../../errors/error-handler.ts";
3
+ import { handleMeShow, handleMeTeams } from "../../handlers/me-handlers.ts";
4
+ import { getGlobalOptions } from "../globals.ts";
5
+
6
+ export function createMeCommand(): Command {
7
+ const me = new Command("me").description("View your profile and teams");
8
+
9
+ me.command("show")
10
+ .description("Show your Geekbot profile")
11
+ .addHelpText("after", "\nExamples:\n geekbot me show")
12
+ .action(async function (this: Command) {
13
+ const globalOpts = getGlobalOptions(this);
14
+ try {
15
+ await handleMeShow(globalOpts);
16
+ } catch (error) {
17
+ handleError(error, globalOpts.debug);
18
+ }
19
+ });
20
+
21
+ me.command("teams")
22
+ .description("List teams you belong to")
23
+ .addHelpText("after", "\nExamples:\n geekbot me teams")
24
+ .action(async function (this: Command) {
25
+ const globalOpts = getGlobalOptions(this);
26
+ try {
27
+ await handleMeTeams(globalOpts);
28
+ } catch (error) {
29
+ handleError(error, globalOpts.debug);
30
+ }
31
+ });
32
+
33
+ return me;
34
+ }