@testchimp/cli 0.1.82 → 0.1.83

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 CHANGED
@@ -48,11 +48,15 @@ testchimp promote-perf-baseline --run-id 01ABC --env-class CI
48
48
  testchimp compare-perf-to-baseline --run-id 01ABC --max-p95-regression-percent 10
49
49
  testchimp list-related-perf-tests --scenario-titles "Checkout,Refund"
50
50
  testchimp list-api-operation-interactions --operation-id 01XYZ --interaction-type REAL --limit 100
51
+ testchimp list-meeting-filter-options
52
+ testchimp list-meetings --from 2026-09-01 --to 2026-09-30 --domain customer.com --label Sales --search "pricing"
53
+ testchimp get-meeting-transcript --meeting-id <meeting-id> --summary-only
51
54
  ```
52
55
 
53
56
  - **stdout:** API response JSON.
54
57
  - **stderr:** progress for `provision-ephemeral-environment-and-wait` (“still waiting…” polls).
55
58
  - **performance gate:** `compare-perf-to-baseline` still prints its JSON response but exits nonzero when `regressed` is `true` (top-level or under `comparison`).
59
+ - **Meetings:** `list-meetings` / `list-meeting-filter-options` cover team-wide Meeting Bots meetings only (same filters as the Meetings page). `--from` / `--to` take `YYYY-MM-DD` (inclusive local days), ISO datetimes, or epoch millis; `--label`, `--participant`, `--domain` are repeatable or comma-separated.
56
60
  - **Flags:** default for each subcommand; **`--json-input '<json>'`** or **`--json-input @file.json`** merges over flags (JSON wins on conflicts). Use JSON for nested bodies (e.g. TrueCoverage scopes).
57
61
 
58
62
  ## Migration from `testchimp-mcp-client`
@@ -521,6 +521,55 @@ export function buildCliProgram() {
521
521
  const out = await runTool("get-meeting-set", { meetingSetId: String(merged.meetingSetId).trim() }, { postMcp });
522
522
  console.log(out);
523
523
  });
524
+ const collectRepeatable = (value, previous = []) => [
525
+ ...previous,
526
+ ...value.split(",").map((s) => s.trim()).filter(Boolean),
527
+ ];
528
+ program
529
+ .command("list-meetings")
530
+ .description(TOOL_DEFINITIONS.find((t) => t.kebab === "list-meetings").description)
531
+ .addOption(jsonInputOption())
532
+ .option("--from <date>", "inclusive start: YYYY-MM-DD (local start of day), ISO datetime, or epoch millis")
533
+ .option("--to <date>", "inclusive end: YYYY-MM-DD (local end of day), ISO datetime, or epoch millis")
534
+ .option("--label <label>", "label filter (repeatable or comma-separated; OR, case-insensitive)", collectRepeatable)
535
+ .option("--participant <emailOrUserId>", "participant filter by email or user id (repeatable or comma-separated; OR)", collectRepeatable)
536
+ .option("--domain <domain>", "participant email domain filter (repeatable or comma-separated; OR)", collectRepeatable)
537
+ .option("--search <text>", "full-text search over title + transcript")
538
+ .option("--page-size <n>", "page size (default 50, max 200; max 25 when searching)")
539
+ .option("--page-token <token>", "nextPageToken from the previous page")
540
+ .action(async (opts) => {
541
+ const body = {};
542
+ if (opts.from)
543
+ body.from = String(opts.from).trim();
544
+ if (opts.to)
545
+ body.to = String(opts.to).trim();
546
+ if (opts.label?.length)
547
+ body.labels = opts.label;
548
+ if (opts.participant?.length)
549
+ body.participantKeys = opts.participant;
550
+ if (opts.domain?.length)
551
+ body.participantDomains = opts.domain;
552
+ if (opts.search)
553
+ body.searchText = String(opts.search);
554
+ if (opts.pageSize) {
555
+ const n = Number(opts.pageSize);
556
+ if (!Number.isInteger(n) || n <= 0)
557
+ throw new Error("--page-size must be a positive integer");
558
+ body.pageSize = n;
559
+ }
560
+ if (opts.pageToken)
561
+ body.pageToken = String(opts.pageToken).trim();
562
+ const merged = mergeBodies(body, opts.jsonInput);
563
+ const out = await runTool("list-meetings", merged, { postMcp });
564
+ console.log(out);
565
+ });
566
+ program
567
+ .command("list-meeting-filter-options")
568
+ .description(TOOL_DEFINITIONS.find((t) => t.kebab === "list-meeting-filter-options").description)
569
+ .action(async () => {
570
+ const out = await runTool("list-meeting-filter-options", {}, { postMcp });
571
+ console.log(out);
572
+ });
524
573
  program
525
574
  .command("get-issue-details")
526
575
  .description(TOOL_DEFINITIONS.find((t) => t.kebab === "get-issue-details").description)
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Parse a date bound to epoch millis: epoch millis, ISO date (`2026-09-01`), or ISO datetime.
3
+ * Date-only values use local time: start of day, or end of day when `endOfDay` is set
4
+ * (so `--to 2026-09-30` includes meetings on the 30th).
5
+ */
6
+ export declare function parseDateBoundToMillis(raw: string | number, endOfDay?: boolean): number;
@@ -0,0 +1,30 @@
1
+ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
2
+ const EPOCH_MILLIS = /^\d{10,}$/;
3
+ /**
4
+ * Parse a date bound to epoch millis: epoch millis, ISO date (`2026-09-01`), or ISO datetime.
5
+ * Date-only values use local time: start of day, or end of day when `endOfDay` is set
6
+ * (so `--to 2026-09-30` includes meetings on the 30th).
7
+ */
8
+ export function parseDateBoundToMillis(raw, endOfDay = false) {
9
+ if (typeof raw === "number") {
10
+ if (!Number.isFinite(raw))
11
+ throw new Error(`Invalid date: ${raw}`);
12
+ return Math.trunc(raw);
13
+ }
14
+ const value = String(raw).trim();
15
+ if (EPOCH_MILLIS.test(value))
16
+ return Number(value);
17
+ if (DATE_ONLY.test(value)) {
18
+ const [y, m, d] = value.split("-").map(Number);
19
+ const date = endOfDay ? new Date(y, m - 1, d, 23, 59, 59, 999) : new Date(y, m - 1, d, 0, 0, 0, 0);
20
+ if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {
21
+ throw new Error(`Invalid date: ${value}`);
22
+ }
23
+ return date.getTime();
24
+ }
25
+ const millis = Date.parse(value);
26
+ if (Number.isNaN(millis)) {
27
+ throw new Error(`Invalid date: ${value} (use YYYY-MM-DD, an ISO datetime, or epoch millis)`);
28
+ }
29
+ return millis;
30
+ }
@@ -296,6 +296,19 @@ export declare const getMeetingTranscriptInput: z.ZodObject<{
296
296
  export declare const getMeetingSetInput: z.ZodObject<{
297
297
  meetingSetId: z.ZodString;
298
298
  }, z.core.$strip>;
299
+ export declare const listMeetingsInput: z.ZodObject<{
300
+ from: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
301
+ to: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
302
+ startDateMillis: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
303
+ endDateMillis: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
304
+ labels: z.ZodOptional<z.ZodArray<z.ZodString>>;
305
+ participantKeys: z.ZodOptional<z.ZodArray<z.ZodString>>;
306
+ participantDomains: z.ZodOptional<z.ZodArray<z.ZodString>>;
307
+ searchText: z.ZodOptional<z.ZodString>;
308
+ pageSize: z.ZodOptional<z.ZodNumber>;
309
+ pageToken: z.ZodOptional<z.ZodString>;
310
+ }, z.core.$strip>;
311
+ export declare const listMeetingFilterOptionsInput: z.ZodObject<{}, z.core.$strip>;
299
312
  export declare const getIssueDetailsInput: z.ZodObject<{
300
313
  issueId: z.ZodString;
301
314
  }, z.core.$strip>;
@@ -177,6 +177,29 @@ export const getMeetingSetInput = z.object({
177
177
  /** ULID from `/testchimp using meeting-set context <id>` (Meetings page → Start Chat) */
178
178
  meetingSetId: z.string().min(1),
179
179
  });
180
+ export const listMeetingsInput = z.object({
181
+ /** Inclusive start bound: YYYY-MM-DD (local start of day), ISO datetime, or epoch millis. */
182
+ from: z.union([z.string().min(1), z.number()]).optional(),
183
+ /** Inclusive end bound: YYYY-MM-DD (local end of day), ISO datetime, or epoch millis. */
184
+ to: z.union([z.string().min(1), z.number()]).optional(),
185
+ /** Raw inclusive start (epoch millis); `from` wins when both are set. */
186
+ startDateMillis: z.union([z.string().min(1), z.number()]).optional(),
187
+ /** Raw inclusive end (epoch millis); `to` wins when both are set. */
188
+ endDateMillis: z.union([z.string().min(1), z.number()]).optional(),
189
+ /** OR filter, case-insensitive (exact values from list-meeting-filter-options). */
190
+ labels: z.array(z.string().min(1)).optional(),
191
+ /** OR filter: participant user ids or emails (keys from list-meeting-filter-options). */
192
+ participantKeys: z.array(z.string().min(1)).optional(),
193
+ /** OR filter: participant email domains (e.g. customer.com). */
194
+ participantDomains: z.array(z.string().min(1)).optional(),
195
+ /** Full-text search over title + transcript (web-search syntax: quotes, OR, -exclude). */
196
+ searchText: z.string().optional(),
197
+ /** Default 50, max 200 (max 25 when searchText is set). */
198
+ pageSize: z.number().int().positive().optional(),
199
+ /** nextPageToken from the previous page. */
200
+ pageToken: z.string().min(1).optional(),
201
+ });
202
+ export const listMeetingFilterOptionsInput = z.object({});
180
203
  export const getIssueDetailsInput = z.object({
181
204
  /** Accepts #B-123, B-123, #B123, B123, or plain 123 */
182
205
  issueId: z.string().min(1),
@@ -2,6 +2,7 @@ import { normalizeScope } from "./normalize.js";
2
2
  import { runProvisionEphemeralEnvironmentAndWait } from "./ephemeralWait.js";
3
3
  import * as S from "./schemas.js";
4
4
  import { resolveGitHeadSha } from "./gitSha.js";
5
+ import { parseDateBoundToMillis } from "./dates.js";
5
6
  import { buildAgentTraceabilityPayload, resolveToolchainVersions } from "./agentTraceability.js";
6
7
  function platformToProtoEnum(platform) {
7
8
  switch (platform) {
@@ -415,6 +416,51 @@ export const TOOL_DEFINITIONS = [
415
416
  });
416
417
  },
417
418
  },
419
+ {
420
+ kebab: "list-meetings",
421
+ description: "List / search cloud-synced Meeting Bots meetings, newest first, like the Meetings page " +
422
+ "filters + search. Team-wide meetings only (visibility: all team members). Filters: from / to " +
423
+ "(YYYY-MM-DD, ISO datetime, or epoch millis; inclusive), labels, participantKeys (user id or " +
424
+ "email), participantDomains, searchText (full-text over title + transcript; hits include a " +
425
+ "searchSnippet with matches wrapped in ⟦ ⟧). Returns meetingId, title, startMillis, labels, " +
426
+ "participants, summaryStatus; page with nextPageToken. Use list-meeting-filter-options for exact " +
427
+ "label / domain / participant values, then get-meeting-transcript (summaryOnly first) per hit.",
428
+ inputSchema: S.listMeetingsInput,
429
+ execute: async (args, { postMcp }) => {
430
+ const a = args;
431
+ const body = {};
432
+ const start = a.from ?? a.startDateMillis;
433
+ const end = a.to ?? a.endDateMillis;
434
+ if (start != null)
435
+ body.startDateMillis = String(parseDateBoundToMillis(start));
436
+ if (end != null)
437
+ body.endDateMillis = String(parseDateBoundToMillis(end, true));
438
+ if (a.labels?.length)
439
+ body.labels = a.labels.map((v) => v.trim());
440
+ if (a.participantKeys?.length)
441
+ body.participantKeys = a.participantKeys.map((v) => v.trim());
442
+ if (a.participantDomains?.length) {
443
+ body.participantDomains = a.participantDomains.map((v) => v.trim().replace(/^@/, ""));
444
+ }
445
+ if (a.searchText?.trim())
446
+ body.searchText = a.searchText.trim();
447
+ if (a.pageSize != null)
448
+ body.pageSize = a.pageSize;
449
+ if (a.pageToken)
450
+ body.pageToken = a.pageToken;
451
+ return postMcp("/api/mcp/list_meetings", body);
452
+ },
453
+ },
454
+ {
455
+ kebab: "list-meeting-filter-options",
456
+ description: "List the labels, participants (key = user id or email, with display name), and participant " +
457
+ "email domains that appear on team-wide Meeting Bots meetings. Use the exact values as " +
458
+ "list-meetings filters (labels, participantKeys, participantDomains).",
459
+ inputSchema: S.listMeetingFilterOptionsInput,
460
+ execute: async (_args, { postMcp }) => {
461
+ return postMcp("/api/mcp/list_meeting_filter_options", {});
462
+ },
463
+ },
418
464
  {
419
465
  kebab: "get-issue-details",
420
466
  description: "Fetch a TestChimp issue (bug) by ordinal id. Accepts flexible issueId formats: " +
@@ -99,3 +99,67 @@ describe("get-meeting-set", () => {
99
99
  });
100
100
  });
101
101
  });
102
+ describe("list-meetings", () => {
103
+ async function capture(args) {
104
+ let request;
105
+ await runTool("list-meetings", args, {
106
+ postMcp: async (path, body) => {
107
+ request = { path, body };
108
+ return "{}";
109
+ },
110
+ });
111
+ return request;
112
+ }
113
+ it("maps filters and converts date-only bounds to inclusive local-day millis", async () => {
114
+ const request = await capture({
115
+ from: "2026-09-01",
116
+ to: "2026-09-30",
117
+ labels: ["Sales "],
118
+ participantKeys: ["buyer@customer.com"],
119
+ participantDomains: ["@customer.com"],
120
+ searchText: " pricing ",
121
+ pageSize: 10,
122
+ pageToken: "tok",
123
+ });
124
+ assert.deepEqual(request, {
125
+ path: "/api/mcp/list_meetings",
126
+ body: {
127
+ startDateMillis: String(new Date(2026, 8, 1, 0, 0, 0, 0).getTime()),
128
+ endDateMillis: String(new Date(2026, 8, 30, 23, 59, 59, 999).getTime()),
129
+ labels: ["Sales"],
130
+ participantKeys: ["buyer@customer.com"],
131
+ participantDomains: ["customer.com"],
132
+ searchText: "pricing",
133
+ pageSize: 10,
134
+ pageToken: "tok",
135
+ },
136
+ });
137
+ });
138
+ it("accepts ISO datetimes and raw epoch millis, and omits empty filters", async () => {
139
+ const request = await capture({ from: "2026-09-01T10:00:00Z", endDateMillis: "1790000000000" });
140
+ assert.deepEqual(request, {
141
+ path: "/api/mcp/list_meetings",
142
+ body: {
143
+ startDateMillis: String(Date.parse("2026-09-01T10:00:00Z")),
144
+ endDateMillis: "1790000000000",
145
+ },
146
+ });
147
+ assert.deepEqual(await capture({}), { path: "/api/mcp/list_meetings", body: {} });
148
+ });
149
+ it("rejects unparseable dates", async () => {
150
+ await assert.rejects(() => capture({ from: "last tuesday" }), /Invalid date/);
151
+ await assert.rejects(() => capture({ to: "2026-02-30" }), /Invalid date/);
152
+ });
153
+ });
154
+ describe("list-meeting-filter-options", () => {
155
+ it("posts an empty body to list_meeting_filter_options", async () => {
156
+ let request;
157
+ await runTool("list-meeting-filter-options", {}, {
158
+ postMcp: async (path, body) => {
159
+ request = { path, body };
160
+ return "{}";
161
+ },
162
+ });
163
+ assert.deepEqual(request, { path: "/api/mcp/list_meeting_filter_options", body: {} });
164
+ });
165
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testchimp/cli",
3
- "version": "0.1.82",
3
+ "version": "0.1.83",
4
4
  "description": "TestChimp CLI and MCP server — coverage, plans, EaaS, TrueCoverage, API operations (calls /api/mcp/*)",
5
5
  "type": "module",
6
6
  "main": "dist/bin/testchimp.js",