@testchimp/cli 0.1.82 → 0.1.84

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)
@@ -766,8 +815,11 @@ export function buildCliProgram() {
766
815
  .option("--tests-folder-path <path>")
767
816
  .option("--plans-folder-path <path>")
768
817
  .option("--repository-full-name <name>")
818
+ .option("--plans-branch <branch>", "Branch plans sync against (empty string = repository default)")
769
819
  .action(async (opts) => {
770
820
  const body = {};
821
+ if (opts.plansBranch !== undefined)
822
+ body.plansBranch = opts.plansBranch;
771
823
  if (opts.testsFolderPath)
772
824
  body.tests_folder_path = opts.testsFolderPath;
773
825
  if (opts.plansFolderPath)
@@ -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>;
@@ -781,6 +794,7 @@ export declare const updateGitFolderMappingInput: z.ZodObject<{
781
794
  testsFolderPath: z.ZodOptional<z.ZodString>;
782
795
  plansFolderPath: z.ZodOptional<z.ZodString>;
783
796
  repositoryFullName: z.ZodOptional<z.ZodString>;
797
+ plansBranch: z.ZodOptional<z.ZodString>;
784
798
  tests_folder_path: z.ZodOptional<z.ZodString>;
785
799
  plans_folder_path: z.ZodOptional<z.ZodString>;
786
800
  repository_full_name: z.ZodOptional<z.ZodString>;
@@ -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),
@@ -300,6 +323,10 @@ export const updateGitFolderMappingInput = z.object({
300
323
  testsFolderPath: z.string().min(1).optional(),
301
324
  plansFolderPath: z.string().min(1).optional(),
302
325
  repositoryFullName: z.string().optional(),
326
+ plansBranch: z
327
+ .string()
328
+ .optional()
329
+ .describe("Branch plans sync against. Empty string resets to the repository default branch."),
303
330
  tests_folder_path: z.string().min(1).optional(),
304
331
  plans_folder_path: z.string().min(1).optional(),
305
332
  repository_full_name: z.string().optional(),
@@ -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: " +
@@ -1473,13 +1519,13 @@ export const TOOL_DEFINITIONS = [
1473
1519
  },
1474
1520
  {
1475
1521
  kebab: "get-git-folder-mapping",
1476
- description: "Return git provider, repository, and mapped plans/tests folder paths for the project.",
1522
+ description: "Return git provider, repository, mapped plans/tests folder paths, and plans branch (empty = repository default) for the project.",
1477
1523
  inputSchema: S.emptyInput,
1478
1524
  execute: async (_args, { postMcp }) => postMcp("/api/mcp/get_git_folder_mapping", {}),
1479
1525
  },
1480
1526
  {
1481
1527
  kebab: "update-git-folder-mapping",
1482
- description: "Update mapped plans/tests folder paths (and optional repository) on the platform. Agent scaffolds folders in a PR; this records platform mapping.",
1528
+ description: "Update mapped plans/tests folder paths (and optional repository / plans branch) on the platform. Agent scaffolds folders in a PR; this records platform mapping.",
1483
1529
  inputSchema: S.updateGitFolderMappingInput,
1484
1530
  execute: async (args, { postMcp }) => {
1485
1531
  const a = args;
@@ -1493,6 +1539,8 @@ export const TOOL_DEFINITIONS = [
1493
1539
  body.plans_folder_path = plans;
1494
1540
  if (repo)
1495
1541
  body.repository_full_name = repo;
1542
+ if (a.plansBranch !== undefined)
1543
+ body.plansBranch = a.plansBranch.trim();
1496
1544
  return postMcp("/api/mcp/update_git_folder_mapping", body);
1497
1545
  },
1498
1546
  },
@@ -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.84",
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",
@@ -33,10 +33,10 @@
33
33
  "typescript": "^6.0.2"
34
34
  },
35
35
  "overrides": {
36
- "fast-uri": "3.1.7",
36
+ "fast-uri": "3.1.8",
37
37
  "qs": "6.16.0",
38
- "ip-address": "10.3.1",
39
- "hono": "4.12.34",
38
+ "ip-address": "10.7.1",
39
+ "hono": "4.13.7",
40
40
  "@hono/node-server": "1.19.17",
41
41
  "body-parser": "2.3.0"
42
42
  },