geekbot-cli 0.2.2 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geekbot-cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "CLI tool for managing Geekbot standups, reports, and polls — designed for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,13 +13,19 @@ export async function resolveCredential(
13
13
  ): Promise<CredentialResult> {
14
14
  // Priority 1: --api-key flag
15
15
  if (options.apiKeyFlag) {
16
- return { apiKey: options.apiKeyFlag.trim(), source: "flag" };
16
+ const trimmed = options.apiKeyFlag.trim();
17
+ if (trimmed) {
18
+ return { apiKey: trimmed, source: "flag" };
19
+ }
17
20
  }
18
21
 
19
22
  // Priority 2: GEEKBOT_API_KEY env var
20
23
  const envKey = process.env.GEEKBOT_API_KEY;
21
24
  if (envKey) {
22
- return { apiKey: envKey.trim(), source: "env" };
25
+ const trimmed = envKey.trim();
26
+ if (trimmed) {
27
+ return { apiKey: trimmed, source: "env" };
28
+ }
23
29
  }
24
30
 
25
31
  // Priority 3: OS keychain
@@ -11,8 +11,8 @@ export function createReportCommand(): Command {
11
11
  .description("List reports with optional filters")
12
12
  .option("--standup-id <id>", "Filter by standup ID")
13
13
  .option("--user-id <id>", "Filter by user ID")
14
- .option("--before <date>", "Reports before date (ISO 8601 or unix timestamp)")
15
- .option("--after <date>", "Reports after date (ISO 8601 or unix timestamp)")
14
+ .option("--before <date>", "Reports before date (YYYY-MM-DD or unix timestamp)")
15
+ .option("--after <date>", "Reports after date (YYYY-MM-DD or unix timestamp)")
16
16
  .option("--limit <n>", "Max number of reports to return")
17
17
  .addHelpText(
18
18
  "after",
@@ -99,6 +99,18 @@ async function enrichNotFound(
99
99
  await fn(client);
100
100
  } catch (error) {
101
101
  if (error instanceof CliError && error.code === "not_found") {
102
+ // Don't enrich user-not-member errors with standup-ID suggestions
103
+ const isUserNotMember = /user is not member/i.test(error.message);
104
+ if (isUserNotMember) {
105
+ throw new CliError(
106
+ error.message,
107
+ error.code,
108
+ error.exitCode,
109
+ error.retryable,
110
+ "The specified user is not a member of this standup. Check members with: geekbot standup get <id>",
111
+ error.context,
112
+ );
113
+ }
102
114
  const suggestion = await buildNotFoundSuggestion(client, resourceType);
103
115
  if (suggestion) {
104
116
  throw new CliError(
@@ -117,11 +129,14 @@ async function enrichNotFound(
117
129
 
118
130
  // ── Handlers ──────────────────────────────────────────────────────────
119
131
 
120
- /** Brief standup projection — only essential fields for discovery */
132
+ /** Brief standup projection — essential fields for discovery */
121
133
  export interface StandupBrief {
122
134
  id: number;
123
135
  name: string;
124
136
  channel: string;
137
+ time: string;
138
+ timezone: string;
139
+ days: string[];
125
140
  }
126
141
 
127
142
  /**
@@ -173,6 +188,9 @@ export async function handleStandupList(
173
188
  id: s.id,
174
189
  name: s.name,
175
190
  channel: s.channel,
191
+ time: s.time,
192
+ timezone: s.timezone,
193
+ days: s.days,
176
194
  }));
177
195
  writeOutput(successList(brief));
178
196
  return;
@@ -352,9 +370,12 @@ export async function handleStandupReplace(
352
370
  const previousStandup = StandupSchema.parse(prevRaw);
353
371
 
354
372
  // Build full body — PUT requires complete representation
355
- const time = options.time ?? "10:00";
373
+ // Carry forward from previous standup when flags are omitted
374
+ const time = options.time ?? previousStandup.time.slice(0, 5);
356
375
  validateTimeFormat(time);
357
- const days = validateDayAbbreviations((options.days ?? "Mon,Tue,Wed,Thu,Fri").split(","));
376
+ const days = options.days
377
+ ? validateDayAbbreviations(options.days.split(","))
378
+ : previousStandup.days;
358
379
 
359
380
  const body: Record<string, unknown> = {
360
381
  name: options.name,
@@ -363,9 +384,7 @@ export async function handleStandupReplace(
363
384
  days,
364
385
  };
365
386
 
366
- if (options.timezone !== undefined) {
367
- body.timezone = options.timezone;
368
- }
387
+ body.timezone = options.timezone ?? previousStandup.timezone;
369
388
 
370
389
  // questions: use provided or carry forward from existing standup
371
390
  if (options.questions !== undefined) {
@@ -378,11 +397,14 @@ export async function handleStandupReplace(
378
397
  body.users = validateSlackIdList(options.users, "user ID");
379
398
  body.sync_channel_members = false;
380
399
  } else {
381
- body.sync_channel_members = true;
400
+ body.users = previousStandup.users.map((u) => u.id);
401
+ body.sync_channel_members = previousStandup.sync_channel_members ?? false;
382
402
  }
383
403
 
384
404
  if (options.waitTime !== undefined) {
385
405
  body.wait_time = validateWaitTime(options.waitTime);
406
+ } else {
407
+ body.wait_time = previousStandup.wait_time;
386
408
  }
387
409
 
388
410
  const raw = await client.put<unknown>(`/v1/standups/${numericId}`, body);
@@ -522,10 +544,14 @@ function buildReplaceUndoCommand(id: number, prev: Standup): string {
522
544
  parts.push(`--days ${shellEscape(prev.days.join(","))}`);
523
545
  }
524
546
 
525
- if (prev.wait_time > 0) {
547
+ if (prev.wait_time !== 0) {
526
548
  parts.push(`--wait-time ${prev.wait_time}`);
527
549
  }
528
550
 
551
+ if (prev.users.length > 0) {
552
+ parts.push(`--users ${prev.users.map((u) => u.id).join(",")}`);
553
+ }
554
+
529
555
  if (prev.questions.length > 0) {
530
556
  parts.push(`--questions ${shellEscape(JSON.stringify(prev.questions.map((q) => q.text)))}`);
531
557
  }
@@ -6,7 +6,7 @@ const ReportAnswerSchema = z.object({
6
6
  id: z.number(),
7
7
  question: z.string().optional().default(""),
8
8
  question_id: z.number().optional(),
9
- answer: z.string(),
9
+ answer: z.string().nullable(),
10
10
  answer_type: z.string().optional().default("text"),
11
11
  images: z
12
12
  .array(
@@ -34,7 +34,7 @@ const TimelineReportRawSchema = z.object({
34
34
  slack_ts: z.string().nullable().optional(),
35
35
  channel: z.string().optional().default(""),
36
36
  questions: z.array(ReportAnswerSchema),
37
- member: CompactUserSchema,
37
+ member: CompactUserSchema.optional(),
38
38
  is_anonymous: z.boolean().optional().default(false),
39
39
  broadcast_thread: z.boolean().optional().default(false),
40
40
  is_confidential: z.boolean().optional().default(false),
@@ -49,11 +49,11 @@ const TimelineReportRawSchema = z.object({
49
49
  const SubmittedReportRawSchema = z.object({
50
50
  id: z.number(),
51
51
  standup_id: z.number(),
52
- timestamp: z.number(),
52
+ timestamp: z.number().nullable(),
53
53
  slack_ts: z.string().nullable().optional(),
54
54
  started_at: z.number().optional(),
55
55
  done_at: z.number().optional(),
56
- broadcasted_at: z.number().nullable().optional(),
56
+ broadcasted_at: z.string().nullable().optional(),
57
57
  channel: z.string().optional().default(""),
58
58
  member: z
59
59
  .object({
@@ -89,7 +89,7 @@ function normalizeTimelineReport(raw: z.output<typeof TimelineReportRawSchema>):
89
89
  standup_id: raw.standup_id,
90
90
  created_at: new Date(raw.timestamp * 1000).toISOString(),
91
91
  questions: raw.questions,
92
- member: raw.member, // Already normalized by CompactUserSchema (profileImg -> profile_img)
92
+ member: raw.member ?? null, // Already normalized by CompactUserSchema (profileImg -> profile_img)
93
93
  is_anonymous: raw.is_anonymous,
94
94
  standup_name: raw.standup_name,
95
95
  };
@@ -103,7 +103,7 @@ export const SubmittedReportSchema = SubmittedReportRawSchema.transform(
103
103
  (raw): Report => ({
104
104
  id: raw.id,
105
105
  standup_id: raw.standup_id,
106
- created_at: new Date(raw.timestamp * 1000).toISOString(),
106
+ created_at: new Date((raw.timestamp ?? raw.done_at ?? Date.now() / 1000) * 1000).toISOString(),
107
107
  questions: raw.answers, // POST uses "answers" key, normalize to "questions"
108
108
  member: raw.member
109
109
  ? {
@@ -50,7 +50,7 @@ const StandupRawSchema = z.object({
50
50
 
51
51
  /** Normalize a raw standup: convert wait_time from seconds to minutes */
52
52
  function normalizeStandup(raw: z.output<typeof StandupRawSchema>) {
53
- return { ...raw, wait_time: raw.wait_time / 60 }; // NORM: seconds -> minutes (Pitfall 1)
53
+ return { ...raw, wait_time: raw.wait_time === -1 ? -1 : raw.wait_time / 60 }; // NORM: seconds -> minutes; -1 is "exact time" sentinel
54
54
  }
55
55
 
56
56
  /** Normalized standup -- wait_time converted from seconds to minutes */
@@ -197,7 +197,7 @@ export function parseDateFilter(raw: string, label: string): string {
197
197
  "validation_error",
198
198
  ExitCode.VALIDATION,
199
199
  false,
200
- "Accepted formats: ISO 8601 (2024-01-15) or unix timestamp (1705276800)",
200
+ "Accepted formats: YYYY-MM-DD (2024-01-15) or unix timestamp (1705276800)",
201
201
  );
202
202
  }
203
203
 
@@ -208,7 +208,7 @@ export function parseDateFilter(raw: string, label: string): string {
208
208
  "validation_error",
209
209
  ExitCode.VALIDATION,
210
210
  false,
211
- "Accepted formats: ISO 8601 (2024-01-15) or unix timestamp (1705276800)",
211
+ "Accepted formats: YYYY-MM-DD (2024-01-15) or unix timestamp (1705276800)",
212
212
  );
213
213
  }
214
214
 
@@ -226,7 +226,7 @@ export function parseDateFilter(raw: string, label: string): string {
226
226
  "validation_error",
227
227
  ExitCode.VALIDATION,
228
228
  false,
229
- "Accepted formats: ISO 8601 (2024-01-15) or unix timestamp (1705276800)",
229
+ "Accepted formats: YYYY-MM-DD (2024-01-15) or unix timestamp (1705276800)",
230
230
  );
231
231
  }
232
232
 
@@ -41,10 +41,14 @@ export function buildDeleteUndoCommand(standup: Standup): string {
41
41
  parts.push(`--days ${shellEscape(standup.days.join(","))}`);
42
42
  }
43
43
 
44
- if (standup.wait_time > 0) {
44
+ if (standup.wait_time !== 0) {
45
45
  parts.push(`--wait-time ${standup.wait_time}`);
46
46
  }
47
47
 
48
+ if (standup.users.length > 0) {
49
+ parts.push(`--users ${standup.users.map((u) => u.id).join(",")}`);
50
+ }
51
+
48
52
  if (standup.questions.length > 0) {
49
53
  parts.push(`--questions ${shellEscape(JSON.stringify(standup.questions.map((q) => q.text)))}`);
50
54
  }
@@ -60,6 +64,8 @@ const FIELD_TO_FLAG: Record<string, string> = {
60
64
  timezone: "--timezone",
61
65
  days: "--days",
62
66
  wait_time: "--wait-time",
67
+ users: "--users",
68
+ questions: "--questions",
63
69
  };
64
70
 
65
71
  /**
@@ -83,6 +89,12 @@ export function buildUpdateUndoCommand(
83
89
  parts.push(`${flag} ${shellEscape(prevValue.slice(0, 5))}`);
84
90
  } else if (key === "days" && Array.isArray(prevValue)) {
85
91
  parts.push(`${flag} ${shellEscape(prevValue.join(","))}`);
92
+ } else if (key === "users" && Array.isArray(prevValue)) {
93
+ parts.push(`${flag} ${prevValue.map((u: { id: string }) => u.id).join(",")}`);
94
+ } else if (key === "questions" && Array.isArray(prevValue)) {
95
+ parts.push(
96
+ `${flag} ${shellEscape(JSON.stringify(prevValue.map((q: { text: string }) => q.text)))}`,
97
+ );
86
98
  } else if (typeof prevValue === "number") {
87
99
  parts.push(`${flag} ${prevValue}`);
88
100
  } else if (typeof prevValue === "string") {
@@ -70,13 +70,14 @@ export function validateSlackIdList(value: string, label: string): string[] {
70
70
  */
71
71
  export function validateWaitTime(value: string): number {
72
72
  const num = Number(value);
73
+ if (num === -1) return num; // -1 is "exact time" sentinel
73
74
  if (!Number.isSafeInteger(num) || num < 0) {
74
75
  throw new CliError(
75
- `Invalid wait time: "${value}". Must be a non-negative integer.`,
76
+ `Invalid wait time: "${value}". Must be a non-negative integer or -1 for exact time.`,
76
77
  "validation_error",
77
78
  ExitCode.VALIDATION,
78
79
  false,
79
- `Provide a numeric value in minutes, e.g.: --wait-time 15`,
80
+ `Provide a numeric value in minutes, e.g.: --wait-time 15 (or -1 for exact time)`,
80
81
  );
81
82
  }
82
83
  return num;