@testchimp/cli 0.1.81 → 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`
@@ -485,16 +485,89 @@ export function buildCliProgram() {
485
485
  .description(TOOL_DEFINITIONS.find((t) => t.kebab === "get-meeting-transcript").description)
486
486
  .addOption(jsonInputOption())
487
487
  .option("--meeting-id <id>", "meeting id (calendar event id, or URL hash for ad-hoc; same as Studio folder under ~/.testchimp/data/meetings/)")
488
+ .option("--summary-only", "return only the post-meeting summary (omit the transcript body)")
488
489
  .action(async (opts) => {
489
490
  const body = {};
490
491
  if (opts.meetingId) {
491
492
  body.meetingId = String(opts.meetingId).trim();
492
493
  }
494
+ if (opts.summaryOnly) {
495
+ body.summaryOnly = true;
496
+ }
493
497
  const merged = mergeBodies(body, opts.jsonInput);
494
498
  if (!merged.meetingId || String(merged.meetingId).trim() === "") {
495
499
  throw new Error("meetingId is required (--meeting-id)");
496
500
  }
497
- const out = await runTool("get-meeting-transcript", { meetingId: String(merged.meetingId).trim() }, { postMcp });
501
+ const out = await runTool("get-meeting-transcript", {
502
+ meetingId: String(merged.meetingId).trim(),
503
+ ...(merged.summaryOnly ? { summaryOnly: true } : {}),
504
+ }, { postMcp });
505
+ console.log(out);
506
+ });
507
+ program
508
+ .command("get-meeting-set")
509
+ .description(TOOL_DEFINITIONS.find((t) => t.kebab === "get-meeting-set").description)
510
+ .addOption(jsonInputOption())
511
+ .option("--meeting-set-id <id>", "meeting-set ULID (from `/testchimp using meeting-set context <id>`)")
512
+ .action(async (opts) => {
513
+ const body = {};
514
+ if (opts.meetingSetId) {
515
+ body.meetingSetId = String(opts.meetingSetId).trim();
516
+ }
517
+ const merged = mergeBodies(body, opts.jsonInput);
518
+ if (!merged.meetingSetId || String(merged.meetingSetId).trim() === "") {
519
+ throw new Error("meetingSetId is required (--meeting-set-id)");
520
+ }
521
+ const out = await runTool("get-meeting-set", { meetingSetId: String(merged.meetingSetId).trim() }, { postMcp });
522
+ console.log(out);
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 });
498
571
  console.log(out);
499
572
  });
500
573
  program
@@ -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
+ }
@@ -291,7 +291,24 @@ export declare const getManualSessionDetailsInput: z.ZodObject<{
291
291
  }, z.core.$strip>;
292
292
  export declare const getMeetingTranscriptInput: z.ZodObject<{
293
293
  meetingId: z.ZodString;
294
+ summaryOnly: z.ZodOptional<z.ZodBoolean>;
294
295
  }, z.core.$strip>;
296
+ export declare const getMeetingSetInput: z.ZodObject<{
297
+ meetingSetId: z.ZodString;
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>;
295
312
  export declare const getIssueDetailsInput: z.ZodObject<{
296
313
  issueId: z.ZodString;
297
314
  }, z.core.$strip>;
@@ -170,7 +170,36 @@ export const getManualSessionDetailsInput = z.object({
170
170
  export const getMeetingTranscriptInput = z.object({
171
171
  /** Calendar event id, or URL hash for ad-hoc meetings */
172
172
  meetingId: z.string().min(1),
173
- });
173
+ /** Return only the post-meeting summary (transcript body omitted). */
174
+ summaryOnly: z.boolean().optional(),
175
+ });
176
+ export const getMeetingSetInput = z.object({
177
+ /** ULID from `/testchimp using meeting-set context <id>` (Meetings page → Start Chat) */
178
+ meetingSetId: z.string().min(1),
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({});
174
203
  export const getIssueDetailsInput = z.object({
175
204
  /** Accepts #B-123, B-123, #B123, B123, or plain 123 */
176
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) {
@@ -388,16 +389,78 @@ export const TOOL_DEFINITIONS = [
388
389
  {
389
390
  kebab: "get-meeting-transcript",
390
391
  description: "Fetch a cloud-synced Meeting Bots transcript by meeting id (calendar event id, or URL hash " +
391
- "for ad-hoc meetings). Prefer local ~/.testchimp/data/meetings/<meeting-id>/transcript.md " +
392
+ "for ad-hoc meetings). Returns title, start time, post-meeting summary, and transcript. " +
393
+ "Use summaryOnly to fetch just the summary (much smaller) and pull the full transcript only " +
394
+ "when the summary is not enough. Prefer local ~/.testchimp/data/meetings/<meeting-id>/transcript.md " +
392
395
  "when present on Studio / the recording machine.",
393
396
  inputSchema: S.getMeetingTranscriptInput,
394
397
  execute: async (args, { postMcp }) => {
395
398
  const a = args;
396
399
  return postMcp("/api/mcp/get_meeting_transcript", {
397
400
  meetingId: a.meetingId,
401
+ ...(a.summaryOnly ? { summaryOnly: true } : {}),
398
402
  });
399
403
  },
400
404
  },
405
+ {
406
+ kebab: "get-meeting-set",
407
+ description: "Fetch a meeting-set context by id (ULID from `/testchimp using meeting-set context <id>`, " +
408
+ "created on the Meetings page via Start Chat). Returns the filters / search text applied and " +
409
+ "the meetings in scope (id, title, start). Fetch each meeting with get-meeting-transcript " +
410
+ "(summaryOnly first). Sets expire after 7 days.",
411
+ inputSchema: S.getMeetingSetInput,
412
+ execute: async (args, { postMcp }) => {
413
+ const a = args;
414
+ return postMcp("/api/mcp/get_meeting_set", {
415
+ meetingSetId: a.meetingSetId,
416
+ });
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
+ },
401
464
  {
402
465
  kebab: "get-issue-details",
403
466
  description: "Fetch a TestChimp issue (bug) by ordinal id. Accepts flexible issueId formats: " +
@@ -73,4 +73,93 @@ describe("get-meeting-transcript", () => {
73
73
  });
74
74
  assert.deepEqual(JSON.parse(result), JSON.parse(response));
75
75
  });
76
+ it("passes summaryOnly when requested", async () => {
77
+ let body;
78
+ await runTool("get-meeting-transcript", { meetingId: "evt-123", summaryOnly: true }, {
79
+ postMcp: async (_path, b) => {
80
+ body = b;
81
+ return "{}";
82
+ },
83
+ });
84
+ assert.deepEqual(body, { meetingId: "evt-123", summaryOnly: true });
85
+ });
86
+ });
87
+ describe("get-meeting-set", () => {
88
+ it("posts meetingSetId to get_meeting_set", async () => {
89
+ let request;
90
+ await runTool("get-meeting-set", { meetingSetId: "01J9Z3X5V4ABCDEF0123456789" }, {
91
+ postMcp: async (path, body) => {
92
+ request = { path, body };
93
+ return "{}";
94
+ },
95
+ });
96
+ assert.deepEqual(request, {
97
+ path: "/api/mcp/get_meeting_set",
98
+ body: { meetingSetId: "01J9Z3X5V4ABCDEF0123456789" },
99
+ });
100
+ });
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
+ });
76
165
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testchimp/cli",
3
- "version": "0.1.81",
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",