@myapihq/cli 2.19.2 → 2.20.1

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.
@@ -14,6 +14,21 @@ export declare function _captureFrom(flags: Flags): sdkFeedback.WidgetCapture |
14
14
  export declare function list(flags: Flags): Promise<void>;
15
15
  export declare function create(bodyArg: string | undefined, flags: Flags): Promise<void>;
16
16
  export declare function resolve(id: string, flags: Flags): Promise<void>;
17
+ /**
18
+ * Turn a report into a Playwright spec.
19
+ *
20
+ * A report that carries a route, an element and an ordered trace of clicks,
21
+ * console errors and failed requests already IS a test — someone hit the bug
22
+ * and recorded the steps. The inversion that makes it a test rather than a
23
+ * replay: the errors in the trace are the reported failure, so the generated
24
+ * assertions are that those errors do NOT happen. It fails on the reported
25
+ * state and passes once the bug is fixed.
26
+ *
27
+ * Prints the spec to stdout so it can be redirected, and takes `--out` for the
28
+ * common case of wanting it on disk. Nothing else is printed to stdout, or a
29
+ * redirect would capture the chatter along with the code.
30
+ */
31
+ export declare function test(id: string | undefined, flags: Flags): Promise<void>;
17
32
  export declare function del(id: string | undefined, flags: Flags): Promise<void>;
18
33
  export declare function widget(sub: string | undefined, arg: string | undefined, flags: Flags): Promise<void>;
19
34
  export declare const SUBCOMMAND_USAGE: Record<string, string>;
@@ -8,11 +8,13 @@ import { feedback as sdkFeedback } from '@myapihq/sdk';
8
8
  import { requireConfig } from '../config.js';
9
9
  import { success, error, info, printJson, banner } from '../output.js';
10
10
  import { formatDate } from '../utils.js';
11
+ import * as fs from 'node:fs';
11
12
  import { requireOrg, requireArg, confirmDestructive } from '../helpers.js';
12
13
  export const EXPOSES = [
13
14
  'GET /feedback/orgs/{org_id}/items',
14
15
  'POST /feedback/orgs/{org_id}/items',
15
16
  'POST /feedback/orgs/{org_id}/items/{id}/resolve',
17
+ 'GET /feedback/orgs/{org_id}/items/{id}/test',
16
18
  'DELETE /feedback/orgs/{org_id}/items/{id}',
17
19
  'GET /feedback/orgs/{org_id}/widgets',
18
20
  'PATCH /feedback/orgs/{org_id}/widgets/{id}',
@@ -34,6 +36,8 @@ export const SCHEMA = {
34
36
  status: 'string',
35
37
  body: 'string',
36
38
  'page-url': 'string',
39
+ base: 'string',
40
+ out: 'string',
37
41
  route: 'string',
38
42
  origins: 'string',
39
43
  // Capture controls. Paired --x / --no-x rather than --x=true|false so that
@@ -217,6 +221,39 @@ export async function resolve(id, flags) {
217
221
  success(`Resolved ${id}`);
218
222
  info('(An unknown id answers the same way, so this is not confirmation the item existed.)');
219
223
  }
224
+ /**
225
+ * Turn a report into a Playwright spec.
226
+ *
227
+ * A report that carries a route, an element and an ordered trace of clicks,
228
+ * console errors and failed requests already IS a test — someone hit the bug
229
+ * and recorded the steps. The inversion that makes it a test rather than a
230
+ * replay: the errors in the trace are the reported failure, so the generated
231
+ * assertions are that those errors do NOT happen. It fails on the reported
232
+ * state and passes once the bug is fixed.
233
+ *
234
+ * Prints the spec to stdout so it can be redirected, and takes `--out` for the
235
+ * common case of wanting it on disk. Nothing else is printed to stdout, or a
236
+ * redirect would capture the chatter along with the code.
237
+ */
238
+ export async function test(id, flags) {
239
+ const usage = 'myapi feedback test <id> [--base https://staging.example.com] [--out <path>]';
240
+ const config = requireConfig();
241
+ const orgId = requireOrg(flags, config, usage);
242
+ requireArg(id, 'id', usage);
243
+ const base = typeof flags.base === 'string' ? flags.base : undefined;
244
+ const spec = await sdkFeedback.getItemTest(config.api_key, orgId, id, base);
245
+ const out = typeof flags.out === 'string' ? flags.out : undefined;
246
+ if (!out) {
247
+ // stdout, unadorned: `myapi feedback test <id> > tests/bug.spec.ts`.
248
+ process.stdout.write(spec.endsWith('\n') ? spec : `${spec}\n`);
249
+ return;
250
+ }
251
+ fs.writeFileSync(out, spec.endsWith('\n') ? spec : `${spec}\n`);
252
+ success(`Wrote ${out}`);
253
+ // It asserts the reported errors do NOT happen, so a red run here is the bug
254
+ // reproducing — not a broken test. Worth saying before someone "fixes" it.
255
+ info('It should FAIL until the bug is fixed — the assertions are that the reported errors stop happening.');
256
+ }
220
257
  export async function del(id, flags) {
221
258
  const usage = 'myapi feedback delete <id> [--yes] [--org <id>]';
222
259
  const config = requireConfig();
@@ -308,34 +345,18 @@ export async function widget(sub, arg, flags) {
308
345
  }
309
346
  if (Object.keys(theme).length)
310
347
  patch.theme = theme;
311
- // Read-modify-write, because the backend REPLACES `capture` rather than
312
- // merging it. Verified against production 2026-08-20: set
313
- // {screenshot:false, text:false}, PATCH {text:true}, and screenshot comes
314
- // back TRUE. Only the capture object behaves this way — a name-only PATCH
315
- // leaves it untouched — so the request as a whole merges and this field
316
- // does not.
348
+ // Only the fields the operator named. The backend merges them into the
349
+ // stored value (myapi-hq#137, live 2026-08-20), so the one they did not
350
+ // mention keeps whatever it was.
317
351
  //
318
- // --help and the skill both promise "omitted flags are left alone". Sending
319
- // the partial object would make `--no-text` silently switch screenshots
320
- // back ON for a widget that had them off: the exact harm this feature
321
- // exists to prevent, on the pages it was built for.
322
- //
323
- // Remove once the backend merges (cross-repo prompt, 2026-08-20): then
324
- // `patch.capture = capture` stands alone and the extra read disappears.
352
+ // Until that shipped this had to read the widget first and send the
353
+ // resolved object, because `capture` replaced rather than merged and
354
+ // `--no-text` would switch screenshots back ON. The read is gone; the
355
+ // guarantee is the server's now, which also closes the race two concurrent
356
+ // updates had.
325
357
  const capture = _captureFrom(flags);
326
- if (capture) {
358
+ if (capture)
327
359
  patch.capture = capture;
328
- try {
329
- const current = (await sdkFeedback.listWidgets(config.api_key, orgId)).find(x => x.id === arg);
330
- if (current?.capture)
331
- patch.capture = { ...current.capture, ...capture };
332
- }
333
- catch {
334
- // A failed read must not block the change that was asked for. The
335
- // operator still gets what they typed, and the resolved state printed
336
- // after the call is the truth either way.
337
- }
338
- }
339
360
  if (Object.keys(patch).length === 0) {
340
361
  error('Nothing to change. Pass at least one of --routes, --origins, --name, --accent, --position, --label, --screenshot/--no-screenshot, --text/--no-text.');
341
362
  }
@@ -365,6 +386,19 @@ export async function widget(sub, arg, flags) {
365
386
  error('Usage: myapi feedback widget create <name> [--origins <list>] [--no-screenshot] [--no-text]\n myapi feedback widget list\n myapi feedback widget update <id> [--routes <list>] [--origins <list>] [--no-screenshot] [--no-text]\n myapi feedback widget revoke <id>');
366
387
  }
367
388
  export const SUBCOMMAND_USAGE = {
389
+ 'test': `myapi feedback test <id> [--base https://staging.example.com] [--out <path>]
390
+
391
+ Render a report as a Playwright spec. A report carrying a route, an element and
392
+ an ordered trace of clicks, console errors and failed requests already IS a
393
+ test; this transcribes it.
394
+
395
+ The assertions are that the reported errors do NOT happen, so it FAILS on the
396
+ reported state and passes once the bug is fixed — the guard arrives with the
397
+ bug. Prints to stdout so it can be redirected; --out writes it instead.
398
+
399
+ --base overrides where the test runs. Without it the report's own page_url is
400
+ used, and a report filed without one answers 422 BASE_URL_REQUIRED rather than
401
+ guessing a host and producing a spec that tests nothing.`,
368
402
  'delete': `myapi feedback delete <id> [--yes] [--org <id>]
369
403
 
370
404
  Erases an item. \`resolve\` closes one and keeps it; this removes it — for spam,
@@ -411,6 +445,7 @@ Subcommands:
411
445
  delete <id> Erase an item (resolve keeps it; this does not)
412
446
  list List feedback, newest first (--kind, --status, --limit, --offset)
413
447
  resolve <id> Close a piece of feedback
448
+ test <id> Render the report as a Playwright spec (--base, --out)
414
449
  widget create <name> Mint a PUBLIC widget key for a site (--origins to restrict)
415
450
  widget list List widgets with the script tag to paste
416
451
  widget revoke <id> Revoke a widget key; collected feedback is kept
@@ -429,6 +464,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
429
464
  case 'list': return list(flags);
430
465
  case 'delete': return del(args[0], flags);
431
466
  case 'resolve': return resolve(args[0], flags);
467
+ case 'test': return test(args[0], flags);
432
468
  case 'widget': return widget(args[0], args[1], flags);
433
469
  default: error(`Unknown subcommand: ${subcommand}. Run "myapi feedback --help" for a list of valid subcommands.`);
434
470
  }
@@ -61,7 +61,7 @@ export const SUBCOMMANDS = {
61
61
  payments: ['connect', 'status', 'charge', 'list', 'get', 'refund'],
62
62
  container: ['create', 'deploy', 'list', 'get', 'logs', 'domain', 'delete'],
63
63
  git: ['create', 'list', 'get', 'delete', 'refs', 'log', 'show', 'tree', 'blob', 'diff', 'commit', 'create-branch', 'delete-branch', 'tag', 'merge', 'repack'],
64
- feedback: ['create', 'list', 'resolve', 'widget'],
64
+ feedback: ['create', 'list', 'resolve', 'test', 'widget'],
65
65
  queue: ['create', 'list', 'get', 'delete', 'enqueue', 'jobs', 'job'],
66
66
  task: ['create', 'list', 'get', 'claim', 'extend', 'resolve', 'fail', 'cancel'],
67
67
  completion: ['install', 'uninstall'],
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ // requestAll follows a keyset cursor to the end.
2
+ //
3
+ // The backend started paging ten list endpoints on 2026-08-20. Callers using
4
+ // `request` kept working and quietly began receiving only the first page — 50
5
+ // rows where they used to get the collection, with nothing in the response
6
+ // saying so. A list that silently stops is worse than a slow one: the caller
7
+ // acts on a partial answer believing it is complete.
8
+ //
9
+ // So these pin the two ways this helper could reintroduce that: stopping early,
10
+ // and looping forever on a server that never says it is done.
11
+ import { describe, it, expect, vi, afterEach } from 'vitest';
12
+ import { requestAll } from '@myapihq/sdk';
13
+ /** A stub that pages `rows` and records every URL it was asked for. */
14
+ function pagingServer(rows, pageSize) {
15
+ const seen = [];
16
+ const fetchMock = vi.fn(async (url) => {
17
+ seen.push(url);
18
+ const u = new URL(url);
19
+ const cursor = u.searchParams.get('cursor');
20
+ const start = cursor ? rows.findIndex(r => r.id === cursor) + 1 : 0;
21
+ const slice = rows.slice(start, start + pageSize);
22
+ const last = start + pageSize >= rows.length;
23
+ return new Response(JSON.stringify({
24
+ success: true,
25
+ data: slice,
26
+ error: null,
27
+ meta: last ? { has_more: false } : { has_more: true, next_cursor: slice[slice.length - 1]?.id },
28
+ }), { status: 200, headers: { 'content-type': 'application/json' } });
29
+ });
30
+ vi.stubGlobal('fetch', fetchMock);
31
+ return { seen };
32
+ }
33
+ afterEach(() => vi.unstubAllGlobals());
34
+ describe('requestAll', () => {
35
+ it('returns every row across pages, in order, exactly once', async () => {
36
+ const rows = Array.from({ length: 23 }, (_, i) => ({ id: `r${i}` }));
37
+ pagingServer(rows, 5);
38
+ const got = await requestAll('https://api.test/things', 'k', { pageSize: 5 });
39
+ expect(got.map(r => r.id)).toEqual(rows.map(r => r.id));
40
+ expect(new Set(got.map(r => r.id)).size).toBe(23);
41
+ });
42
+ it('passes the cursor on, so it is not just asking page one repeatedly', async () => {
43
+ const rows = Array.from({ length: 7 }, (_, i) => ({ id: `r${i}` }));
44
+ const { seen } = pagingServer(rows, 3);
45
+ await requestAll('https://api.test/things', 'k', { pageSize: 3 });
46
+ expect(seen).toHaveLength(3);
47
+ expect(seen[0]).toContain('limit=3');
48
+ expect(seen[0]).not.toContain('cursor=');
49
+ expect(seen[1]).toContain('cursor=r2');
50
+ expect(seen[2]).toContain('cursor=r5');
51
+ });
52
+ it('stops on the page that says has_more is false', async () => {
53
+ const rows = Array.from({ length: 4 }, (_, i) => ({ id: `r${i}` }));
54
+ const { seen } = pagingServer(rows, 10);
55
+ const got = await requestAll('https://api.test/things', 'k', { pageSize: 10 });
56
+ expect(got).toHaveLength(4);
57
+ expect(seen).toHaveLength(1);
58
+ });
59
+ it('throws rather than returning a partial list when the server never finishes', async () => {
60
+ // A server that always says has_more with a fresh cursor. Returning what
61
+ // was collected would hand back a truncated list that looks complete —
62
+ // exactly the failure this helper exists to prevent, so it must fail loudly.
63
+ let n = 0;
64
+ vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({
65
+ success: true, data: [{ id: `x${n}` }], error: null,
66
+ meta: { has_more: true, next_cursor: `c${n++}` },
67
+ }), { status: 200, headers: { 'content-type': 'application/json' } })));
68
+ await expect(requestAll('https://api.test/things', 'k', { maxPages: 5 }))
69
+ .rejects.toThrow(/pagination_runaway|still reported more rows/);
70
+ });
71
+ it('throws when the cursor does not advance', async () => {
72
+ // A stuck cursor is the ascending/descending mix-up seen from the client
73
+ // side: the server keeps answering with the same next_cursor, so a naive
74
+ // loop spins forever on the same rows.
75
+ vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({
76
+ success: true, data: [{ id: 'same' }], error: null,
77
+ meta: { has_more: true, next_cursor: 'stuck' },
78
+ }), { status: 200, headers: { 'content-type': 'application/json' } })));
79
+ await expect(requestAll('https://api.test/things', 'k', { maxPages: 50 }))
80
+ .rejects.toThrow(/pagination_stalled|same cursor twice/);
81
+ });
82
+ it('unwraps a wrapped payload when told how', async () => {
83
+ // {queues: […]}, {clients: […]} — the wrappers kept so paging did not break
84
+ // existing callers.
85
+ vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({
86
+ success: true, data: { queues: [{ id: 'q1' }, { id: 'q2' }] }, error: null,
87
+ meta: { has_more: false },
88
+ }), { status: 200, headers: { 'content-type': 'application/json' } })));
89
+ const got = await requestAll('https://api.test/queues', 'k', {
90
+ select: d => (d?.queues ?? []),
91
+ });
92
+ expect(got.map(r => r.id)).toEqual(['q1', 'q2']);
93
+ });
94
+ });
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: my-feedback-api
3
- version: 1.6.0
3
+ version: 1.7.0
4
4
  description: >
5
5
  Collect feedback from the people using what you built. A public widget key lets a page submit without a credential; you list, filter and resolve the results. Kind is chosen by the person reporting, not inferred from their wording.
6
6
  triggers: [feedback, bug report, user feedback, feature request, widget, support, complaints, praise]
7
- checksum: sha256-f8be1cee17e2dc5e1dcf2310cf18d275982d54460af26a0b642fc46bd8c4691e
7
+ checksum: sha256-a76c44ae06a9693ec4874132f52675d0344dfdbb174fff668cb602b16dca59b7
8
8
  ---
9
9
 
10
10
  # MyFeedbackAPI
@@ -19,34 +19,32 @@ Two halves: a **widget key** a page embeds, and the **items** it produces.
19
19
  ### The widget key is public, and that is the point
20
20
 
21
21
  `widget create <name>` mints a key that ships in your page source. It is **not a
22
- secret** it names your org so a visitor can submit without signing in, and
23
- authenticates nobody.
24
-
25
- Treating it as a credential is the mistake: people hide it and the widget cannot
26
- work. It needs `--origins`, so another site cannot post through it:
22
+ secret**: it names your org so a visitor can submit without signing in, and
23
+ authenticates nobody. Treating it as a credential is the mistake — people hide
24
+ it and the widget stops working. It needs `--origins`, so another site cannot
25
+ post through it:
27
26
 
28
27
  ```bash
29
28
  myapi feedback widget create site --origins app.example.com,example.com
30
29
  ```
31
30
 
32
- Revoke with `myapi feedback widget revoke <id>`: submissions stop immediately,
33
- collected feedback is kept.
31
+ `widget revoke <id>`: submissions stop immediately, collected feedback is kept.
34
32
 
35
33
  ### Embedding it: one script tag
36
34
 
37
35
  The platform hosts the widget, compiled per key with its routes and theme baked
38
- in. The intended way in — you build no UI:
36
+ in. The intended way in — no UI to build:
39
37
 
40
38
  ```html
41
39
  <script src="https://api.myapihq.com/feedback/in/<widget_key>/widget.js" async></script>
42
40
  ```
43
41
 
44
- It shows only on the widget's routes — set them with `widget update <id>
45
- --routes /app,/app/**`; live in ~5 min, no redeploy.
42
+ It shows only on the widget's routes — `widget update <id> --routes
43
+ /app,/app/**`; live in ~5 min, no redeploy.
46
44
 
47
- The person points at an element or drags a region; it sends:
45
+ The person points at an element or drags a region; it sends `kind`, `body`,
46
+ `page_url`, `route`, `viewport`, plus:
48
47
 
49
- - `kind`, `body`, `page_url`, `route`, `viewport`
50
48
  - `target_selector` and `target_region` `{x,y,w,h}` — on **every** report: the
51
49
  element's bounding box for a click, the dragged box for a drag
52
50
  - `target_context` — what the element *was*: text, role, the heading above it
@@ -55,32 +53,30 @@ The person points at an element or drags a region; it sends:
55
53
  A wrong key cannot break the page: the response is `application/javascript`
56
54
  with a comment naming the fix, so a typo throws no syntax error.
57
55
 
58
- **Screenshots are automatic you build no capture step.** The widget attaches
59
- the picture as a second call after the report is saved, so an image that fails
60
- to render never costs the report.
56
+ **Screenshots are automatic.** The widget attaches the picture as a second call
57
+ after the report is saved, so an image that fails to render never costs it.
61
58
 
62
59
  The image is **private in your org's assets**; listing returns a **signed link
63
- good for ~an hour** (`screenshot_url`) — re-list rather than persisting it.
64
- PNG/JPEG/WebP up to 3 MB; over that the report is kept without it.
60
+ good for ~an hour** (`screenshot_url`) — re-list rather than persisting it. Up
61
+ to 3 MB; over that the report is kept without it.
65
62
 
66
63
  ### Submitting by hand (only if you cannot use the script)
67
64
 
68
65
  `POST https://api.myapihq.com/feedback/in/<widget_key>` — no auth, no SDK, same
69
- fields the widget sends (`kind`, `body`, `page_url`, …).
70
-
71
- Errors: `WIDGET_NOT_FOUND` (a revoked key reads like an invented one, so keys
72
- cannot be probed), `ORIGIN_NOT_ALLOWED`, `RATE_LIMITED`, `INVALID_KIND`,
73
- `BODY_REQUIRED`, `BODY_TOO_LONG` (>8000 chars), `BODY_TOO_LARGE` (>64 KB, usually
74
- an oversized `trace`). Nothing is saved; the key only writes.
66
+ fields the widget sends (`kind`, `body`, `page_url`, …). Errors:
67
+ `WIDGET_NOT_FOUND` (a revoked key reads like an invented one, so keys cannot be
68
+ probed), `ORIGIN_NOT_ALLOWED`, `RATE_LIMITED`, `INVALID_KIND`, `BODY_REQUIRED`,
69
+ `BODY_TOO_LONG` (>8000), `BODY_TOO_LARGE` (>64 KB, usually an oversized
70
+ `trace`). Nothing is saved; the key only writes.
75
71
 
76
72
  ### Kind is a claim, not a guess
77
73
 
78
74
  `--kind` is one of `bug`, `issue`, `suggestion`, and it is **what the reporter
79
- says it is**. A `bug` means they believe the product is broken — a more urgent
80
- signal than a wish for something new. Do not re-classify from the wording.
75
+ says it is**. A `bug` means they believe the product is broken — more urgent
76
+ than a wish for something new. Do not re-classify from the wording.
81
77
 
82
78
  Classification and duplicate-grouping are designed but not built, so treat
83
- `kind` as the person's own label, not a processed signal.
79
+ `kind` as the person's label, not a processed signal.
84
80
 
85
81
  ### Controlling what a report contains
86
82
 
@@ -94,7 +90,7 @@ myapi feedback widget update <id> --no-text # keep SHAPE, drop words
94
90
 
95
91
  `--no-text` keeps tag, role, id and `data-feedback-id`: on a table row, "row, id
96
92
  `row-8`" not the tenant's name. `--screenshot` / `--text` turn one back on.
97
- Omitted flags are left alone, so one off never changes the other. Live in ~5 min.
93
+ Omitted flags are left alone. Live in ~5 min.
98
94
 
99
95
  **In your page**, two levers only you control:
100
96
 
@@ -104,8 +100,8 @@ window.__myapiFeedback.identify(); // on sign-out
104
100
  ```
105
101
 
106
102
  Callable any time, even long after load. Your id for your user — opaque to us,
107
- never resolved, posted to a public endpoint: a label, not authentication. Shows
108
- as `from` in `feedback list`.
103
+ never resolved, posted to a public endpoint: a label, not auth. Shows as `from`
104
+ in `feedback list`.
109
105
 
110
106
  Mark an element `data-feedback-ignore` to skip it in the context AND black it out
111
107
  in the screenshot.
@@ -118,19 +114,23 @@ in the screenshot.
118
114
  | Different rules per page | two widgets, two route sets |
119
115
 
120
116
  The last row is usually right: capture off on record-bearing routes, on
121
- elsewhere — not one global switch.
117
+ elsewhere.
122
118
 
123
119
  ### Reading it back
124
120
 
125
121
  Each item carries what was pointed at, how they got there, and a picture.
126
122
  `list` summarises it — the element's own text rather than its CSS selector,
127
123
  counts of steps/errors/failed requests, whether a screenshot exists, and `from`
128
- when the page called `identify()`. `--trace` expands the events; `--json` passes
129
- everything through.
124
+ when the page called `identify()`. `--trace` expands events; `--json` passes all.
125
+
126
+ Two not to misread: **`trace[].t` is ms since page load**, not clock time. And
127
+ **`target_context.filled` says an input had a value, never what**.
130
128
 
131
- Two not to misread: **`trace[].t` is ms since page load**, not clock time only
132
- a delta means anything. And **`target_context.filled` says an input had a value,
133
- never what**.
129
+ `feedback test <id>` renders a report as a Playwright spec a report with a
130
+ route, an element and a trace of clicks and errors already IS a test. It asserts
131
+ the reported errors do NOT happen, so it fails now and passes once fixed.
132
+ `--base` says where to run it; without one the report's `page_url` is used, and
133
+ a report lacking that answers 422 `BASE_URL_REQUIRED`.
134
134
 
135
135
  `delete` removes the screenshot with the report — what matters is somebody
136
136
  erasing a report because of what is in the picture.
@@ -140,7 +140,7 @@ erasing a report because of what is in the picture.
140
140
  truncation — both describe the whole match set.
141
141
 
142
142
  `resolve <id>` closes an item. An unknown id answers like an already-resolved
143
- one, so success is not proof it existed — deliberate, so ids cannot be probed.
143
+ one, so success is not proof it existed — so ids cannot be probed.
144
144
  <!-- llm:end -->
145
145
 
146
146
  ## Commands
@@ -150,6 +150,7 @@ one, so success is not proof it existed — deliberate, so ids cannot be probed.
150
150
  | `myapi feedback create "<text>" --kind <k>` | Record one item (`--page-url`, `--route` for context) |
151
151
  | `myapi feedback list [--kind bug\|issue\|suggestion] [--status open\|resolved] [--limit N] [--offset N] [--trace]` | List feedback, newest first. Each report summarises what was pointed at, what happened before, and whether a screenshot exists; `--trace` expands the events |
152
152
  | `myapi feedback resolve <id>` | Close an item, keeping it |
153
+ | `myapi feedback test <id> [--base <url>] [--out <p>]` | Render the report as a Playwright spec (stdout, or `--out`) |
153
154
  | `myapi feedback delete <id>` | Erase an item — for spam, or personal details typed into the box |
154
155
  | `myapi feedback widget create <name> [--origins a.com,b.com]` | Mint a PUBLIC widget key for a site |
155
156
  | `myapi feedback widget list` | List widgets, each with the script tag to paste |
@@ -160,25 +161,19 @@ one, so success is not proof it existed — deliberate, so ids cannot be probed.
160
161
  ## Examples
161
162
  <!-- llm:start -->
162
163
  ```bash
163
- # 1. Mint a key for your site. Restrict the origins.
164
+ # Mint a key for your site, restrict the origins, paste the tag it prints
164
165
  myapi feedback widget create marketing --origins example.com,www.example.com
165
- # → prints a PUBLIC key to embed in the page
166
-
167
- # 2. See your widgets and the exact tag to paste
168
166
  myapi feedback widget list
169
167
 
170
- # 3. Change where it appears, or what it may collect — same key either way
168
+ # Where it appears, and what it may collect — same key either way
171
169
  myapi feedback widget update <id> --routes /app,/app/**
172
170
  myapi feedback widget update <id> --no-screenshot --no-text # record-bearing routes
173
171
 
174
- # 4. Read what came back, newest first (`from` shows if the page identify()d)
175
- myapi feedback list --status open
176
- myapi feedback list --kind bug --limit 20
177
-
178
- # 5. Record something yourself (support call, your own testing)
179
- myapi feedback create "checkout 500s on retry" --kind bug --route /checkout
172
+ # Read what came back (`from` shows when the page called identify())
173
+ myapi feedback list --status open --kind bug
180
174
 
181
- # 6. Close it or delete it, which also removes its screenshot
175
+ # Turn a report into a failing Playwright spec, then close or erase it
176
+ myapi feedback test <id> --out tests/regression.spec.ts
182
177
  myapi feedback resolve <id>
183
178
  myapi feedback delete <id> --yes
184
179
  ```
@@ -186,11 +181,10 @@ myapi feedback delete <id> --yes
186
181
 
187
182
  ## Notes
188
183
 
189
- - The widget key is public by design — restrict with `--origins`, don't hide it.
184
+ - The key is public by design — restrict with `--origins`, don't hide it.
190
185
  - Feedback survives widget revocation.
191
186
  - **`resolve` keeps, `delete` erases.** A public inbox collects spam and the odd
192
- report with personal details in it; `delete` is how those leave. Both are
193
- idempotent and answer the same for an unknown id, so ids cannot be probed.
187
+ report with personal details; `delete` is how those leave. Both idempotent.
194
188
 
195
189
  ## HTTP (from deployed code)
196
190
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
3
  "license": "Apache-2.0",
4
- "version": "2.19.2",
4
+ "version": "2.20.1",
5
5
  "description": "MyAPI command-line interface",
6
6
  "repository": {
7
7
  "type": "git",
@@ -46,7 +46,7 @@
46
46
  "lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
47
47
  },
48
48
  "dependencies": {
49
- "@myapihq/sdk": "^2.19.2"
49
+ "@myapihq/sdk": "^2.20.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^25.6.0",